From d0b7849c0283b9836d526b64bbdca9a222ed4221 Mon Sep 17 00:00:00 2001 From: Leon <1172300059@qq.com> Date: Fri, 16 Oct 2026 15:02:25 +0800 Subject: [PATCH] fix: restore skin controls and desktop portrait compatibility Respect host root layout, draw the scrim once, discover appearance controls without build hashes, and recover failed portraits independently. Add unit and real-browser regression coverage. Refs #8, #12, #15, #10, #13. Native Desktop acceptance remains pending; no release. --- README.md | 6 +- docs/compatibility-validation.md | 35 ++ lib/client.js | 550 +++++++++++++++++--------- lib/client.js.map | 8 +- package-lock.json | 14 + package.json | 4 +- scripts/test-client-compatibility.mjs | 280 +++++++++++++ scripts/test-client-runtime.mjs | 307 ++++++++++++++ src/client/appearance.ts | 208 ++++++++++ src/client/index.tsx | 238 ++--------- src/client/portrait-loader.test.ts | 214 ++++++++++ src/client/portrait-loader.ts | 201 ++++++++++ src/client/skin.css | 24 +- 13 files changed, 1690 insertions(+), 399 deletions(-) create mode 100644 docs/compatibility-validation.md create mode 100644 scripts/test-client-compatibility.mjs create mode 100644 scripts/test-client-runtime.mjs create mode 100644 src/client/appearance.ts create mode 100644 src/client/portrait-loader.test.ts create mode 100644 src/client/portrait-loader.ts diff --git a/README.md b/README.md index eb1a0f7..291bc52 100644 --- a/README.md +++ b/README.md @@ -60,10 +60,11 @@ dsh plugin --profile web add 'github:kingOfSoySauce/dsh-liang-skin' - `滑动变祖`:显示滑块并启用人物、背景和自适应配色。 - `原生`:移除背景层、皮肤变量和滑块,恢复 Harness 原生界面。 +- `滑动变祖绑定思考等级`:关闭后滑块只切换人物和配色,不修改模型的推理等级。 皮肤保持启用时,人物与背景配色由滑块档位决定;切换 DSH 原生深浅主题不会改变当前档位的背景。 -选择保存在当前浏览器本地,不会改动模型配置。 +绑定开关保存在当前浏览器本地。切回原生外观只对本次客户端激活有效;刷新页面或重新激活插件后默认开启皮肤。外观选择的跨刷新记忆暂未实现。绑定开启时,滑块会提交模型的推理档位;关闭绑定后只改变外观。 @@ -134,10 +135,13 @@ dsh plugin --profile web remove dsh-client-liang-intensity-skin npm install npm test npm run build +npm run test:compatibility ``` 修改 client 源码后需要运行 `npm run build`,并一起提交更新后的 `lib/client.js` 和 source map。 +浏览器兼容性回归使用本机 Chrome / Chromium;可通过 `CHROME_PATH` 指定浏览器可执行文件,不会自动下载浏览器。测试使用宿主布局夹具,不能代替官方 Desktop 真机验收。本轮未发布修复的验证步骤见 [兼容性验证说明](docs/compatibility-validation.md)。 + ## 灵感与素材来源 本插件的“滑动变祖”视觉概念与人物图片素材源自 diff --git a/docs/compatibility-validation.md b/docs/compatibility-validation.md new file mode 100644 index 0000000..831edbf --- /dev/null +++ b/docs/compatibility-validation.md @@ -0,0 +1,35 @@ +# 未发布的兼容性修复验证 + +本轮以已发布的 0.1.10 为基线,修复留在独立分支,不发布 npm 或版本标签。 + +## 变更 + +- 保留宿主的根容器定位和层级,背景使用独立层级;兼容社区 Desktop 的标题栏高度。 +- 保留官方 Windows 框架透明与 macOS 侧栏、新会话按钮配色修复;半透明背景遮罩统一绘制一次。 +- 外观入口按原生三个主题按钮的结构识别,兼容构建类名哈希变化;停用时清理入口和监听。 +- 人像逐张加载和解码,单张失败不阻止其他帧使用;失败后分别等待 0.5、1.5、3 秒重试,最多额外三次。 +- 未加载的目标帧保留上一张可用人像;全部失败时保留配色背景。停用时取消重试和请求,重新启用后重新加载。 +- 保持素材原色、面板透明度及原有推理等级绑定语义。本轮不处理跨刷新外观记忆,也不修改皮肤市场。 + +## 本地验证包 + +验证包是完整的 `.tgz`,包含客户端构建与 24 张 WebP 人像;只用于本地验证。具体包路径、校验值和对应修复提交由交付消息提供。 + +官方 Desktop:使用当前宿主支持的本地包安装入口加载该 `.tgz`,由用户点击安装、启用和重启;不要把 Web profile 的安装结果当成 Desktop 已安装。若当前宿主没有本地包入口,先提供其实际入口信息,再选择安装路径,不直接编辑 profile。 + +DSH Web:可用 `dsh plugin --profile web add /绝对路径/测试包.tgz` 安装。安装或重新安装后完全停止并重新启动对应宿主;浏览器刷新不能替代 Desktop 完全退出重开。 + +## 用户验收 + +1. 安装测试包并完全退出、重开官方 Desktop,确认加载的是测试包;保留其他皮肤停用的状态。 +2. 打开「设置 → 通用设置 → 外观皮肤」:应有「滑动变祖」和推理等级绑定开关;切原生后人物及滑块消失,点击滑动变祖后恢复。 +3. 在浅色档和黑金档分别观察人物、侧栏和输入区;调整窗口大小、切换侧栏,确认无半屏断层、文字可读。 +4. 拖动滑块:人物应随档位改变;关闭绑定后只改变人物和配色,不修改模型推理等级。 +5. 打开「完全权限」、模型选择和其他弹窗,确认显示层级正常且可以点击;标题栏仍可拖动,窗口按钮可用。 +6. 刷新或重启时,外观仍按本次激活默认启用皮肤;跨刷新记住原生选择属于暂缓的 #9。 + +自动化测试使用浏览器宿主夹具并模拟图片失败;它们不代表官方 Windows Desktop 真机已经验收。 + +## Issue / PR 收尾 + +已确认可收尾的 #2、#3、#6 关闭。#8、#12、#14、#15 与 PR #10、#13 在用户验证前保持开放;PR #9 留待后续处理。 diff --git a/lib/client.js b/lib/client.js index e588e98..8d7bf0b 100644 --- a/lib/client.js +++ b/lib/client.js @@ -29,7 +29,7 @@ var import_react = require("react"); // src/client/skin.css var skin_default = `.liang-skin-backdrop { position: fixed; - z-index: 0; + z-index: -1; inset: 0; overflow: hidden; pointer-events: none; @@ -48,7 +48,7 @@ body[data-liang-skin="on"] .liang-skin-backdrop { right: 0; display: block; width: min(42vw, 720px); - height: 100vh; + height: 100%; object-fit: cover; object-position: 50% 42%; opacity: var(--liang-portrait-opacity, 0.45); @@ -75,11 +75,16 @@ body[data-liang-skin="on"] .liang-skin-backdrop { position: absolute; inset: 0; content: ""; - background: linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%); + background: + linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%), + linear-gradient(var(--liang-bg-base, transparent), var(--liang-bg-base, transparent)); } body[data-liang-skin="on"] { - --dsw-alias-bg-base: var(--liang-bg-base) !important; + /* Keep the backdrop above the canvas but below content and native portals. + The host owns root positioning, dimensions, and its own stacking order. */ + isolation: isolate; + --dsw-alias-bg-base: transparent !important; --dsw-alias-bg-layer-1: var(--liang-layer-1) !important; --dsw-alias-bg-layer-2: var(--liang-layer-2) !important; --dsw-alias-bg-layer-3: var(--liang-layer-3) !important; @@ -239,11 +244,16 @@ body[data-liang-skin="on"]:is( } body[data-liang-skin="on"] > #root { - position: relative; - z-index: 1; background: transparent !important; } +body[data-liang-skin="on"]:is( + [data-dsh-desktop-mode="compatibility"], + [data-dsh-desktop-mode="extended"] +) .liang-skin-backdrop { + top: var(--dsh-desktop-frame-height, 0px); +} + /* Windows paints the sidebar fill across the host frame, covering the portrait. Match macOS's transparent frame while keeping the sidebar, caption, and conversation surfaces painted by their own host rules. */ @@ -480,7 +490,7 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb { display: flex; } -[class*="_8HJdBW_cubeRow"]:has(.liang-appearance-choice) { +[class*="_cubeRow"]:has(.liang-appearance-choice) { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -626,6 +636,324 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb { } `; +// src/client/appearance.ts +var NATIVE_APPEARANCE_ROW = '[class*="_cubeRow"]'; +var LIANG_APPEARANCE_BUTTON = "liang-appearance-choice"; +var LIANG_BINDING_CONTROL = "liang-appearance-binding"; +var LIANG_BINDING_INPUT = "liang-appearance-binding__input"; +function hasClassSuffix(node, suffix) { + return [...node.classList].some((name) => name.endsWith(suffix)); +} +function findAppearanceRow() { + for (const row of document.querySelectorAll(NATIVE_APPEARANCE_ROW)) { + if (!hasClassSuffix(row, "_cubeRow")) continue; + const nativeButtons = [...row.children].filter( + (node) => node instanceof HTMLButtonElement && hasClassSuffix(node, "_themeCube") + ); + if (nativeButtons.length < 3) continue; + let group = row.parentElement; + while (group !== null && !hasClassSuffix(group, "_group")) { + group = group.parentElement; + } + group ??= row.parentElement; + if (group !== null) return { row, group, nativeButtons }; + } + return null; +} +function installLiangAppearanceButton(scope, presenter, packageId) { + let pending = false; + let disposed = false; + let mounted = null; + const nativeClickHandlers = /* @__PURE__ */ new Map(); + const unmount = () => { + for (const [button, handler] of nativeClickHandlers) { + button.removeEventListener("click", handler, { capture: true }); + } + nativeClickHandlers.clear(); + if (mounted === null) return; + mounted.button.removeEventListener("click", mounted.handleCustomClick); + mounted.bindingInput.removeEventListener("change", mounted.handleBindingChange); + mounted.button.remove(); + mounted.binding.remove(); + mounted = null; + }; + const choose = async (enabled) => { + if (disposed || pending || scope.getSnapshot().enabled === enabled) return; + pending = true; + sync(); + try { + await presenter.choose(enabled); + } catch { + } finally { + pending = false; + if (!disposed) sync(); + } + }; + const createMount = (row, group) => { + const english = document.documentElement.lang.toLowerCase().startsWith("en"); + const button = document.createElement("button"); + button.className = LIANG_APPEARANCE_BUTTON; + button.type = "button"; + button.dataset.plugin = packageId; + button.setAttribute("aria-label", "\u6ED1\u52A8\u53D8\u7956"); + const icon = document.createElement("span"); + icon.className = "liang-appearance-choice__icon"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "\u25C8"; + const label = document.createElement("span"); + label.className = "liang-appearance-choice__label"; + label.textContent = english ? "Slider" : "\u6ED1\u52A8\u53D8\u7956"; + button.append(icon, label); + const handleCustomClick = () => { + void choose(true); + }; + button.addEventListener("click", handleCustomClick); + const binding = document.createElement("label"); + binding.className = LIANG_BINDING_CONTROL; + binding.dataset.plugin = packageId; + const bindingCopy = document.createElement("span"); + bindingCopy.className = "liang-appearance-binding__copy"; + const bindingLabel = document.createElement("span"); + bindingLabel.className = "liang-appearance-binding__label"; + const bindingText = english ? "Bind slider to reasoning level" : "\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7"; + bindingLabel.textContent = bindingText; + const bindingDescription = document.createElement("span"); + bindingDescription.className = "liang-appearance-binding__description"; + bindingDescription.id = "liang-appearance-binding-description"; + bindingDescription.textContent = english ? "When off, the slider does not change the reasoning level." : "\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7"; + bindingCopy.append(bindingLabel, bindingDescription); + const bindingInput = document.createElement("input"); + bindingInput.className = LIANG_BINDING_INPUT; + bindingInput.type = "checkbox"; + bindingInput.setAttribute("role", "switch"); + bindingInput.setAttribute("aria-label", bindingText); + bindingInput.setAttribute("aria-describedby", bindingDescription.id); + const updateBinding = async () => { + if (disposed || pending) return; + try { + await scope.setBindEffort(bindingInput.checked); + } catch { + } finally { + if (!disposed) sync(); + } + }; + const handleBindingChange = () => { + void updateBinding(); + }; + bindingInput.addEventListener("change", handleBindingChange); + binding.append(bindingCopy, bindingInput); + return { row, group, button, binding, bindingInput, handleCustomClick, handleBindingChange }; + }; + const sync = () => { + if (disposed) return; + const target = findAppearanceRow(); + if (target === null) { + unmount(); + return; + } + if (mounted !== null && (mounted.row !== target.row || mounted.group !== target.group)) { + unmount(); + } + mounted ??= createMount(target.row, target.group); + const { row, group, button, binding, bindingInput } = mounted; + if (button.parentElement !== row) row.append(button); + const snapshot = scope.getSnapshot(); + button.disabled = pending; + button.setAttribute("aria-pressed", String(snapshot.enabled)); + if (snapshot.enabled) { + if (binding.parentElement !== group) group.append(binding); + bindingInput.checked = snapshot.bindEffort; + bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort)); + bindingInput.disabled = pending; + } else { + binding.remove(); + } + for (const [nativeButton, handler] of nativeClickHandlers) { + if (target.nativeButtons.includes(nativeButton)) continue; + nativeButton.removeEventListener("click", handler, { capture: true }); + nativeClickHandlers.delete(nativeButton); + } + for (const nativeButton of target.nativeButtons) { + if (nativeClickHandlers.has(nativeButton)) continue; + const handleNativeClick = () => { + void choose(false); + }; + nativeClickHandlers.set(nativeButton, handleNativeClick); + nativeButton.addEventListener("click", handleNativeClick, { capture: true }); + } + }; + const observer = new MutationObserver(sync); + observer.observe(document.body, { childList: true, subtree: true }); + const unsubscribe = scope.subscribe(sync); + sync(); + return () => { + if (disposed) return; + disposed = true; + observer.disconnect(); + unsubscribe(); + unmount(); + }; +} + +// src/client/portrait-loader.ts +var RETRY_DELAYS = [500, 1500, 3e3]; +function retryUrl(source, retry) { + if (retry === 0) return source; + const hashIndex = source.indexOf("#"); + const hash = hashIndex < 0 ? "" : source.slice(hashIndex); + const base = hashIndex < 0 ? source : source.slice(0, hashIndex); + const queryIndex = base.indexOf("?"); + const path = queryIndex < 0 ? base : base.slice(0, queryIndex); + const query = new URLSearchParams(queryIndex < 0 ? "" : base.slice(queryIndex + 1)); + query.set("retry", String(retry)); + return `${path}?${query}${hash}`; +} +function createPortraitLoader(options) { + const sources = options.sources.map((source) => ({ ...source })); + const assets = /* @__PURE__ */ new Map(); + for (const { url } of sources) { + if (!assets.has(url)) { + assets.set(url, { + url, + status: "idle", + loadedUrl: null, + retries: 0, + image: null, + timer: null, + detach: null + }); + } + } + const createImage = options.createImage ?? (() => new Image()); + let onChange = options.onChange; + let active = false; + let disposed = false; + let generation = 0; + let targetLevel = sources[0]?.level ?? 0; + let displayedIndex = null; + let lastState = null; + const isCurrent = (run) => active && !disposed && generation === run; + const nearestIndex = (available) => { + let nearest = null; + for (let index = 0; index < sources.length; index += 1) { + if (!available(index)) continue; + const distance = Math.abs(sources[index].level - targetLevel); + const previous = nearest === null ? Infinity : Math.abs(sources[nearest].level - targetLevel); + if (nearest === null || distance < previous || distance === previous && sources[index].level > sources[nearest].level) { + nearest = index; + } + } + return nearest; + }; + const isReady = (index) => assets.get(sources[index].url)?.status === "ready"; + const emit = (state) => { + if (lastState?.index === state.index && lastState.url === state.url && lastState.status === state.status) return; + lastState = state; + onChange?.(state); + }; + const reconcile = () => { + if (!active || disposed) return; + const target = nearestIndex(() => true); + if (target !== null && isReady(target)) displayedIndex = target; + else if (displayedIndex === null) displayedIndex = nearestIndex(isReady); + if (displayedIndex !== null) { + const asset = assets.get(sources[displayedIndex].url); + emit({ index: displayedIndex, url: asset.loadedUrl, image: asset.image, status: "ready" }); + } else { + const exhausted = [...assets.values()].every((asset) => asset.status === "failed"); + emit({ index: null, url: null, image: null, status: exhausted ? "failed" : "loading" }); + } + }; + const request = (asset, run) => { + if (!isCurrent(run)) return; + asset.status = "loading"; + const image = createImage(); + asset.image = image; + image.loading = "eager"; + image.decoding = "async"; + const url = retryUrl(asset.url, asset.retries); + let settled = false; + let decoding = false; + const current = () => !settled && isCurrent(run) && asset.image === image; + const detach = () => { + image.removeEventListener("load", handleLoad); + image.removeEventListener("error", handleError); + asset.detach = null; + }; + const handleError = () => { + if (!current()) return; + settled = true; + detach(); + image.removeAttribute("src"); + asset.image = null; + if (asset.retries < RETRY_DELAYS.length) { + const delay = RETRY_DELAYS[asset.retries]; + asset.retries += 1; + asset.timer = setTimeout(() => { + if (!isCurrent(run)) return; + asset.timer = null; + request(asset, run); + }, delay); + } else { + asset.status = "failed"; + reconcile(); + } + }; + const handleLoad = () => { + if (!current() || decoding) return; + decoding = true; + void image.decode().then(() => { + if (!current()) return; + settled = true; + detach(); + asset.loadedUrl = url; + asset.status = "ready"; + reconcile(); + }, handleError); + }; + asset.detach = detach; + image.addEventListener("load", handleLoad); + image.addEventListener("error", handleError); + image.src = url; + }; + const stop = () => { + active = false; + generation += 1; + for (const asset of assets.values()) { + if (asset.timer !== null) clearTimeout(asset.timer); + asset.timer = null; + asset.detach?.(); + asset.image?.removeAttribute("src"); + asset.image = null; + asset.status = "idle"; + asset.loadedUrl = null; + asset.retries = 0; + } + displayedIndex = null; + lastState = null; + }; + return { + start() { + if (disposed || active) return; + active = true; + const run = ++generation; + reconcile(); + for (const asset of assets.values()) request(asset, run); + }, + select(level) { + if (disposed || !Number.isFinite(level)) return; + targetLevel = level; + reconcile(); + }, + stop, + dispose() { + stop(); + disposed = true; + onChange = null; + } + }; +} + // src/client/logic.ts var PREVIEW_MAX_FRAME = 240; var MAX_LEVEL = 30; @@ -665,19 +993,6 @@ function selectedEffortIndex(efforts, selectedId, defaultId) { const id = selectedId ?? defaultId; return id === void 0 ? -1 : efforts.findIndex((effort) => effort.id === id); } -function portraitBlendForLevel(level, anchors) { - if (anchors.length === 0) return { lowerIndex: -1, upperIndex: -1, mix: 0 }; - const safeLevel = Number.isFinite(level) ? Math.min(anchors[anchors.length - 1], Math.max(anchors[0], level)) : anchors[0]; - let upperIndex = anchors.findIndex((anchor) => anchor >= safeLevel); - if (upperIndex < 0) upperIndex = anchors.length - 1; - const lowerIndex = Math.max(0, upperIndex - (anchors[upperIndex] > safeLevel ? 1 : 0)); - const span = anchors[upperIndex] - anchors[lowerIndex]; - return { - lowerIndex, - upperIndex, - mix: span === 0 ? 0 : (safeLevel - anchors[lowerIndex]) / span - }; -} function liangRankForFrame(rawFrame) { const level = clampFrame(rawFrame) / PREVIEW_MAX_FRAME * MAX_LEVEL; const index = level >= MAX_LEVEL ? LIANG_RANKS.length - 1 : Math.floor(level / 6); @@ -750,7 +1065,6 @@ var import_jsx_runtime = require("react/jsx-runtime"); var PACKAGE_ID = "dsh-client-liang-intensity-skin"; var LOCALE_NAMESPACE = "liang.skin"; var ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`; -var FIRST_PORTRAIT_FILE = "portrait-source-v2/stage-00.webp"; var BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort"; var PORTRAIT_ANCHORS = [ { level: 0, file: "stage-00.webp" }, @@ -778,7 +1092,6 @@ var PORTRAIT_ANCHORS = [ { level: 29, file: "level-29.webp" }, { level: 30, file: "stage-30.webp" } ]; -var ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level); var cssVariables = [ "--liang-strength", "--liang-page", @@ -800,9 +1113,7 @@ var SkinPresenter = class { scope; theme; root; - portrait; - preloads; - portraitReady = false; + portraits; enabled = true; // Default to the max frame so the first paint after load is the dark shell; // starting at 0 flashed the light palette before the directory resolved. @@ -817,46 +1128,31 @@ var SkinPresenter = class { this.root = document.createElement("div"); this.root.className = "liang-skin-backdrop"; this.root.dataset.plugin = PACKAGE_ID; - this.root.dataset.media = "sequence"; + this.root.dataset.media = "color"; this.root.setAttribute("aria-hidden", "true"); - this.portrait = document.createElement("img"); - this.portrait.className = "liang-skin-sequence-frame"; - this.portrait.alt = ""; - this.portrait.draggable = false; - this.portrait.decoding = "async"; - this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`; - this.portrait.addEventListener("error", this.handleSequenceError); - this.preloads = PORTRAIT_ANCHORS.map(({ file }) => { - const image = new Image(); - image.loading = "eager"; - image.decoding = "async"; - image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`; - return image; - }); - void Promise.all(this.preloads.map((image) => image.decode())).then( - () => { - if (this.disposed) return; - this.portraitReady = true; + this.portraits = createPortraitLoader({ + sources: PORTRAIT_ANCHORS.map(({ level, file }) => ({ + level, + url: `${ASSET_PREFIX}/portrait-source-v2/${file}` + })), + onChange: ({ image }) => { + if (this.disposed || !this.enabled) return; + if (image === null) { + this.root.dataset.media = "color"; + this.root.replaceChildren(); + return; + } + const portrait = image; + portrait.className = "liang-skin-sequence-frame"; + portrait.alt = ""; + portrait.draggable = false; + if (this.root.firstChild !== portrait) this.root.replaceChildren(portrait); this.root.dataset.media = "sequence"; - this.updatePortrait(paletteForFrame(this.frame).level); - }, - () => { } - ); - this.root.append(this.portrait); - document.body.prepend(this.root); + }); this.unsubscribe = scope.subscribe(() => this.syncSettings()); this.syncSettings(); } - handleSequenceError = () => { - if (this.portrait.src.endsWith(FIRST_PORTRAIT_FILE)) { - this.root.dataset.media = "color"; - return; - } - this.portraitReady = false; - this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`; - this.root.dataset.media = "sequence"; - }; syncSettings() { this.setEnabled(this.scope.getSnapshot().enabled); } @@ -867,19 +1163,27 @@ var SkinPresenter = class { return this.frame; } setEnabled(enabled) { + if (this.disposed) return; this.enabled = enabled; if (enabled) { if (!this.root.isConnected) document.body.prepend(this.root); document.body.dataset.liangSkin = "on"; this.applyFrame(); + this.portraits.start(); } else { + if (this.raf !== 0) cancelAnimationFrame(this.raf); + this.raf = 0; this.root.remove(); + this.portraits.stop(); + this.root.replaceChildren(); + this.root.dataset.media = "color"; delete document.body.dataset.liangSkin; delete document.body.dataset.liangStage; for (const name of cssVariables) document.body.style.removeProperty(name); } } setFrame(frame) { + if (this.disposed) return; this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame))); this.frame = this.pendingFrame; if (!this.enabled) return; @@ -891,6 +1195,7 @@ var SkinPresenter = class { }); } applyFrame() { + if (this.disposed || !this.enabled) return; const palette = paletteForFrame(this.frame); const body = document.body; this.syncNativeTheme(palette.stage === 5 ? "dark" : "light"); @@ -917,16 +1222,7 @@ var SkinPresenter = class { this.theme.setTheme(theme); } updatePortrait(level) { - if (!this.portraitReady) return; - const { lowerIndex, upperIndex, mix: mix2 } = portraitBlendForLevel( - level, - ANCHOR_LEVELS - ); - const lower = PORTRAIT_ANCHORS[lowerIndex]; - const upper = PORTRAIT_ANCHORS[upperIndex]; - const selected = mix2 >= 0.5 ? upper : lower; - const source = `${ASSET_PREFIX}/portrait-source-v2/${selected.file}`; - if (this.portrait.getAttribute("src") !== source) this.portrait.src = source; + this.portraits.select(level); } async choose(enabled) { this.setEnabled(enabled); @@ -942,8 +1238,7 @@ var SkinPresenter = class { this.disposed = true; this.unsubscribe(); if (this.raf !== 0) cancelAnimationFrame(this.raf); - this.portrait.removeEventListener("error", this.handleSequenceError); - for (const image of this.preloads) image.src = ""; + this.portraits.dispose(); this.root.remove(); document.body.removeAttribute("data-liang-skin"); delete document.body.dataset.liangStage; @@ -1110,117 +1405,6 @@ function LiangEffortSlider({ directory, load, select, presenter, scope }) { } ); } -var NATIVE_APPEARANCE_GROUP = '[class*="_8HJdBW_group"]'; -var NATIVE_APPEARANCE_ROW = '[class*="_8HJdBW_cubeRow"]'; -var LIANG_APPEARANCE_BUTTON = "liang-appearance-choice"; -var LIANG_BINDING_CONTROL = "liang-appearance-binding"; -var LIANG_BINDING_INPUT = "liang-appearance-binding__input"; -function installLiangAppearanceButton(scope, presenter) { - let pending = false; - const hookedNativeButtons = /* @__PURE__ */ new Set(); - const nativeClickHandlers = /* @__PURE__ */ new Map(); - const sync = () => { - const group = [...document.querySelectorAll(NATIVE_APPEARANCE_GROUP)].find((node) => node.querySelector('[class*="_8HJdBW_themeCube"]')); - const row = group?.querySelector(NATIVE_APPEARANCE_ROW); - if (row === void 0 || row === null) return; - let customButton = row.querySelector(`.${LIANG_APPEARANCE_BUTTON}`); - if (customButton === null) { - customButton = document.createElement("button"); - customButton.className = LIANG_APPEARANCE_BUTTON; - customButton.type = "button"; - customButton.dataset.plugin = PACKAGE_ID; - customButton.setAttribute("aria-label", "\u6ED1\u52A8\u53D8\u7956"); - const icon = document.createElement("span"); - icon.className = "liang-appearance-choice__icon"; - icon.setAttribute("aria-hidden", "true"); - icon.textContent = "\u25C8"; - const label = document.createElement("span"); - label.className = "liang-appearance-choice__label"; - label.textContent = document.documentElement.lang.toLowerCase().startsWith("en") ? "Slider" : "\u6ED1\u52A8\u53D8\u7956"; - customButton.append(icon, label); - customButton.addEventListener("click", () => { - if (pending || scope.getSnapshot().enabled) return; - pending = true; - sync(); - void presenter.choose(true).finally(() => { - pending = false; - sync(); - }); - }); - } - if (customButton.parentElement !== row) row.append(customButton); - const snapshot = scope.getSnapshot(); - customButton.disabled = pending; - customButton.setAttribute("aria-pressed", String(snapshot.enabled)); - let bindingControl = group.querySelector(`.${LIANG_BINDING_CONTROL}`); - if (!snapshot.enabled) { - bindingControl?.remove(); - bindingControl = null; - } else if (bindingControl === null) { - bindingControl = document.createElement("label"); - bindingControl.className = LIANG_BINDING_CONTROL; - bindingControl.dataset.plugin = PACKAGE_ID; - const bindingCopy = document.createElement("span"); - bindingCopy.className = "liang-appearance-binding__copy"; - const bindingLabel = document.createElement("span"); - bindingLabel.className = "liang-appearance-binding__label"; - const bindingText = document.documentElement.lang.toLowerCase().startsWith("en") ? "Bind slider to reasoning level" : "\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7"; - bindingLabel.textContent = bindingText; - const bindingDescription = document.createElement("span"); - bindingDescription.className = "liang-appearance-binding__description"; - bindingDescription.id = "liang-appearance-binding-description"; - bindingDescription.textContent = document.documentElement.lang.toLowerCase().startsWith("en") ? "When off, the slider does not change the reasoning level." : "\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7"; - bindingCopy.append(bindingLabel, bindingDescription); - const bindingInput = document.createElement("input"); - bindingInput.className = LIANG_BINDING_INPUT; - bindingInput.type = "checkbox"; - bindingInput.setAttribute("role", "switch"); - bindingInput.setAttribute("aria-label", bindingText); - bindingInput.setAttribute("aria-describedby", bindingDescription.id); - bindingInput.addEventListener("change", () => { - void scope.setBindEffort(bindingInput.checked).catch(() => sync()); - }); - bindingControl.append(bindingCopy, bindingInput); - group.append(bindingControl); - } - if (bindingControl !== null) { - const bindingInput = bindingControl.querySelector(`.${LIANG_BINDING_INPUT}`); - if (bindingInput !== null) { - bindingInput.checked = snapshot.bindEffort; - bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort)); - bindingInput.disabled = pending; - } - } - for (const nativeButton of row.querySelectorAll('[class*="_8HJdBW_themeCube"]')) { - if (hookedNativeButtons.has(nativeButton)) continue; - hookedNativeButtons.add(nativeButton); - const handleNativeClick = () => { - if (pending || !scope.getSnapshot().enabled) return; - pending = true; - sync(); - void presenter.choose(false).finally(() => { - pending = false; - sync(); - }); - }; - nativeClickHandlers.set(nativeButton, handleNativeClick); - nativeButton.addEventListener("click", handleNativeClick, { capture: true }); - } - }; - const observer = new MutationObserver(sync); - observer.observe(document.body, { childList: true, subtree: true }); - const unsubscribe = scope.subscribe(sync); - sync(); - return () => { - observer.disconnect(); - unsubscribe(); - for (const [nativeButton, handleNativeClick] of nativeClickHandlers) { - nativeButton.removeEventListener("click", handleNativeClick, { capture: true }); - } - document.querySelectorAll(`.${LIANG_APPEARANCE_BUTTON}`).forEach((button) => button.remove()); - document.querySelectorAll(`.${LIANG_BINDING_CONTROL}`).forEach((control) => control.remove()); - }; -} var inject = [ "slots", "sessions", @@ -1284,7 +1468,7 @@ function apply(ctx) { const presenter = new SkinPresenter(scope, theme); ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter"); ctx.effect( - () => installLiangAppearanceButton(scope, presenter), + () => installLiangAppearanceButton(scope, presenter, PACKAGE_ID), "liang-intensity-skin: native appearance extension" ); ctx.effect( diff --git a/lib/client.js.map b/lib/client.js.map index d38eb31..526afab 100644 --- a/lib/client.js.map +++ b/lib/client.js.map @@ -1,7 +1,7 @@ { "version": 3, - "sources": ["../src/client/index.tsx", "../src/client/skin.css", "../src/client/logic.ts"], - "sourcesContent": ["import {\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport type { Context as ClientContext } from \"@deepseek-ai/cordis\";\nimport type { SnapshotStore } from \"@deepseek-ai/dsh-client-store\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-conversation/client\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-settings/client\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-model-selection/client\";\nimport styles from \"./skin.css\";\nimport {\n PREVIEW_MAX_FRAME,\n frameForEffort,\n indicatorLabel,\n nearestEffortIndex,\n paletteForFrame,\n portraitBlendForLevel,\n selectedEffortIndex,\n type EffortLike,\n} from \"./logic\";\n\nconst PACKAGE_ID = \"dsh-client-liang-intensity-skin\";\nconst LOCALE_NAMESPACE = \"liang.skin\";\nconst ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`;\nconst FIRST_PORTRAIT_FILE = \"portrait-source-v2/stage-00.webp\";\nconst BIND_EFFORT_KEY = \"dsh-liang-intensity-skin.bind-effort\";\n\nconst PORTRAIT_ANCHORS = [\n { level: 0, file: \"stage-00.webp\" },\n { level: 1, file: \"level-01.webp\" },\n { level: 3, file: \"level-03.webp\" },\n { level: 4, file: \"level-04.webp\" },\n { level: 6, file: \"stage-06.webp\" },\n { level: 7, file: \"level-07.webp\" },\n { level: 9, file: \"level-09.webp\" },\n { level: 10, file: \"level-10.webp\" },\n { level: 12, file: \"stage-12.webp\" },\n { level: 13, file: \"level-13.webp\" },\n { level: 14, file: \"level-14.webp\" },\n { level: 15, file: \"bridge-15.webp\" },\n { level: 16, file: \"level-16.webp\" },\n { level: 17, file: \"level-17.webp\" },\n { level: 18, file: \"stage-18.webp\" },\n { level: 19, file: \"level-19.webp\" },\n { level: 21, file: \"level-21.webp\" },\n { level: 22, file: \"level-22.webp\" },\n { level: 24, file: \"stage-24.webp\" },\n { level: 25, file: \"level-25.webp\" },\n { level: 27, file: \"bridge-27.webp\" },\n { level: 28, file: \"level-28.webp\" },\n { level: 29, file: \"level-29.webp\" },\n { level: 30, file: \"stage-30.webp\" },\n] as const;\n\nconst ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level);\n\ninterface SkinSettings {\n enabled: boolean;\n bindEffort: boolean;\n}\n\ninterface PreferenceStore {\n getSnapshot(): SkinSettings;\n subscribe(listener: () => void): () => void;\n set(enabled: boolean): Promise;\n setBindEffort(enabled: boolean): Promise;\n dispose(): void;\n}\n\ntype NativeThemeId = \"light\" | \"dark\";\n\ninterface ThemeService {\n getTheme(): { preference: string };\n setTheme(id: NativeThemeId | \"system\"): void;\n}\n\ninterface ModelSelection {\n provider: string;\n model: string;\n reasoningEffort?: string;\n}\n\ninterface CatalogModel {\n id: string;\n reasoning?: {\n efforts: EffortLike[];\n defaultEffort?: string;\n };\n}\n\ninterface ModelDirectoryState {\n current: ModelSelection | null;\n groups: readonly { id: string; models: CatalogModel[] }[];\n status: \"idle\" | \"loading\" | \"ready\" | \"selecting\" | \"error\";\n error: string | null;\n}\n\ninterface ModelDirectory {\n store: SnapshotStore;\n load(): Promise;\n select(selection: ModelSelection): Promise<{ ok: boolean }>;\n}\n\ninterface SliderProps {\n directory: SnapshotStore;\n load: () => void;\n select: (selection: ModelSelection) => Promise;\n presenter: SkinPresenter;\n scope: PreferenceStore;\n}\n\ninterface SettingsRowProps {\n scope: PreferenceStore;\n presenter: SkinPresenter;\n theme: ThemeService;\n subscribeTheme: (listener: () => void) => () => void;\n t: (key: string) => string;\n}\n\nconst cssVariables = [\n \"--liang-strength\",\n \"--liang-page\",\n \"--liang-bg-base\",\n \"--liang-layer-1\",\n \"--liang-layer-2\",\n \"--liang-layer-3\",\n \"--liang-sidebar\",\n \"--liang-ink\",\n \"--liang-secondary\",\n \"--liang-tertiary\",\n \"--liang-border\",\n \"--liang-accent\",\n \"--liang-accent-hover\",\n \"--liang-hover\",\n \"--liang-portrait-opacity\",\n] as const;\n\nclass SkinPresenter {\n private readonly scope: PreferenceStore;\n private readonly theme: ThemeService;\n private readonly root: HTMLDivElement;\n private readonly portrait: HTMLImageElement;\n private readonly preloads: HTMLImageElement[];\n private portraitReady = false;\n private enabled = true;\n // Default to the max frame so the first paint after load is the dark shell;\n // starting at 0 flashed the light palette before the directory resolved.\n private frame = PREVIEW_MAX_FRAME;\n private pendingFrame = PREVIEW_MAX_FRAME;\n private raf = 0;\n private disposed = false;\n private unsubscribe: () => void;\n\n constructor(scope: PreferenceStore, theme: ThemeService) {\n this.scope = scope;\n this.theme = theme;\n this.root = document.createElement(\"div\");\n this.root.className = \"liang-skin-backdrop\";\n this.root.dataset.plugin = PACKAGE_ID;\n // Show the first half-body frame immediately while the remaining frames\n // are fetched and decoded. A request being complete does not mean the\n // bitmap is ready for a tear-free first swap.\n this.root.dataset.media = \"sequence\";\n this.root.setAttribute(\"aria-hidden\", \"true\");\n\n this.portrait = document.createElement(\"img\");\n this.portrait.className = \"liang-skin-sequence-frame\";\n this.portrait.alt = \"\";\n this.portrait.draggable = false;\n this.portrait.decoding = \"async\";\n this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`;\n this.portrait.addEventListener(\"error\", this.handleSequenceError);\n\n this.preloads = PORTRAIT_ANCHORS.map(({ file }) => {\n const image = new Image();\n image.loading = \"eager\";\n image.decoding = \"async\";\n image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`;\n return image;\n });\n\n // `new Image()` starts the requests, but the browser may still defer\n // decoding until the image is attached to the document. Wait for all\n // frames up front so the first slider interaction never reveals a blank\n // or half-painted frame.\n void Promise.all(this.preloads.map((image) => image.decode())).then(\n () => {\n if (this.disposed) return;\n this.portraitReady = true;\n this.root.dataset.media = \"sequence\";\n this.updatePortrait(paletteForFrame(this.frame).level);\n },\n () => {\n // Keep the already-visible first half-body frame if another optional\n // frame cannot be decoded. It is still a valid skin fallback.\n },\n );\n\n this.root.append(this.portrait);\n document.body.prepend(this.root);\n\n this.unsubscribe = scope.subscribe(() => this.syncSettings());\n this.syncSettings();\n }\n\n private readonly handleSequenceError = () => {\n if (this.portrait.src.endsWith(FIRST_PORTRAIT_FILE)) {\n this.root.dataset.media = \"color\";\n return;\n }\n this.portraitReady = false;\n this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`;\n this.root.dataset.media = \"sequence\";\n };\n\n private syncSettings() {\n this.setEnabled(this.scope.getSnapshot().enabled);\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n getFrame() {\n return this.frame;\n }\n\n setEnabled(enabled: boolean) {\n this.enabled = enabled;\n if (enabled) {\n if (!this.root.isConnected) document.body.prepend(this.root);\n document.body.dataset.liangSkin = \"on\";\n this.applyFrame();\n } else {\n this.root.remove();\n delete document.body.dataset.liangSkin;\n delete document.body.dataset.liangStage;\n for (const name of cssVariables) document.body.style.removeProperty(name);\n }\n }\n\n setFrame(frame: number) {\n this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame)));\n this.frame = this.pendingFrame;\n if (!this.enabled) return;\n if (this.raf !== 0) return;\n this.raf = requestAnimationFrame(() => {\n this.raf = 0;\n this.frame = this.pendingFrame;\n this.applyFrame();\n });\n }\n\n private applyFrame() {\n const palette = paletteForFrame(this.frame);\n const body = document.body;\n this.syncNativeTheme(palette.stage === 5 ? \"dark\" : \"light\");\n body.dataset.liangStage = String(palette.stage);\n body.style.setProperty(\"--liang-strength\", String(palette.strength));\n body.style.setProperty(\"--liang-page\", palette.page);\n body.style.setProperty(\"--liang-bg-base\", palette.base);\n body.style.setProperty(\"--liang-layer-1\", palette.layer1);\n body.style.setProperty(\"--liang-layer-2\", palette.layer2);\n body.style.setProperty(\"--liang-layer-3\", palette.layer3);\n body.style.setProperty(\"--liang-sidebar\", palette.sidebar);\n body.style.setProperty(\"--liang-ink\", palette.ink);\n body.style.setProperty(\"--liang-secondary\", palette.secondary);\n body.style.setProperty(\"--liang-tertiary\", palette.tertiary);\n body.style.setProperty(\"--liang-border\", palette.border);\n body.style.setProperty(\"--liang-accent\", palette.accent);\n body.style.setProperty(\"--liang-accent-hover\", palette.accentHover);\n body.style.setProperty(\"--liang-hover\", palette.hover);\n body.style.setProperty(\"--liang-portrait-opacity\", palette.portraitOpacity);\n this.updatePortrait(palette.level);\n }\n\n syncNativeTheme(theme: NativeThemeId = paletteForFrame(this.frame).stage === 5 ? \"dark\" : \"light\") {\n if (!this.enabled || this.theme.getTheme().preference === theme) return;\n this.theme.setTheme(theme);\n }\n\n private updatePortrait(level: number) {\n if (!this.portraitReady) return;\n const { lowerIndex, upperIndex, mix } = portraitBlendForLevel(\n level,\n ANCHOR_LEVELS,\n );\n const lower = PORTRAIT_ANCHORS[lowerIndex];\n const upper = PORTRAIT_ANCHORS[upperIndex];\n const selected = mix >= 0.5 ? upper : lower;\n const source = `${ASSET_PREFIX}/portrait-source-v2/${selected.file}`;\n if (this.portrait.getAttribute(\"src\") !== source) this.portrait.src = source;\n }\n\n async choose(enabled: boolean) {\n this.setEnabled(enabled);\n try {\n await this.scope.set(enabled);\n } catch (error) {\n this.syncSettings();\n throw error;\n }\n }\n\n dispose() {\n if (this.disposed) return;\n this.disposed = true;\n this.unsubscribe();\n if (this.raf !== 0) cancelAnimationFrame(this.raf);\n this.portrait.removeEventListener(\"error\", this.handleSequenceError);\n for (const image of this.preloads) image.src = \"\";\n this.root.remove();\n document.body.removeAttribute(\"data-liang-skin\");\n delete document.body.dataset.liangStage;\n for (const name of cssVariables) document.body.style.removeProperty(name);\n }\n}\n\nfunction modelReasoning(state: ModelDirectoryState) {\n const current = state.current;\n if (current === null) return null;\n const group = state.groups.find((item) => item.id === current.provider);\n const model = group?.models.find((item) => item.id === current.model);\n if (model?.reasoning === undefined) return null;\n return {\n selection: current,\n efforts: model.reasoning.efforts,\n defaultEffort: model.reasoning.defaultEffort,\n };\n}\n\nfunction LiangEffortSlider({ directory, load, select, presenter, scope }: SliderProps) {\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const skin = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n );\n const reasoning = useMemo(() => modelReasoning(state), [state]);\n const efforts = reasoning?.efforts ?? [];\n const committedIndex = selectedEffortIndex(\n efforts,\n reasoning?.selection.reasoningEffort,\n reasoning?.defaultEffort,\n );\n // An unknown committed effort defaults to the max frame: a fresh\n // conversation paints the dark shell immediately instead of flashing the\n // light palette until the directory load resolves the real effort.\n const committedFrame = committedIndex < 0\n ? PREVIEW_MAX_FRAME\n : frameForEffort(committedIndex, efforts.length);\n const bindEffort = skin.bindEffort;\n const [frame, setFrame] = useState(() => bindEffort ? committedFrame : presenter.getFrame());\n const [pending, setPending] = useState(false);\n const [failed, setFailed] = useState(false);\n const [interacting, setInteracting] = useState(false);\n const dragging = useRef(false);\n const dragStartFrame = useRef(frame);\n const enabled = skin.enabled;\n\n useEffect(() => {\n if (enabled) load();\n }, [enabled, load]);\n\n useEffect(() => {\n if (!bindEffort || dragging.current || pending) return;\n setFrame(committedFrame);\n presenter.setFrame(committedFrame);\n }, [bindEffort, committedFrame, pending, presenter]);\n\n if (!enabled) return null;\n if (bindEffort && (reasoning === null || efforts.length < 2)) return null;\n\n const previewIndex = nearestEffortIndex(frame, efforts);\n const previewEffort = efforts[previewIndex];\n const progressRatio = frame / PREVIEW_MAX_FRAME;\n const tooltipLabel = indicatorLabel(frame, bindEffort ? efforts : []);\n\n const commit = async (rawFrame: number) => {\n if (!bindEffort) {\n dragging.current = false;\n setFrame(rawFrame);\n presenter.setFrame(rawFrame);\n return;\n }\n const targetIndex = nearestEffortIndex(rawFrame, efforts);\n const target = efforts[targetIndex];\n if (target === undefined) return;\n const targetFrame = frameForEffort(targetIndex, efforts.length);\n dragging.current = false;\n setFrame(targetFrame);\n presenter.setFrame(targetFrame);\n if (targetIndex === committedIndex || pending) return;\n setPending(true);\n setFailed(false);\n const accepted = await select({\n provider: reasoning.selection.provider,\n model: reasoning.selection.model,\n reasoningEffort: target.id,\n });\n setPending(false);\n if (!accepted) {\n setFailed(true);\n setFrame(committedFrame);\n presenter.setFrame(committedFrame);\n }\n };\n\n return (\n \n {interacting && (\n \n {tooltipLabel}\n \n )}\n
\n {efforts.map((effort, index) => (\n \n ))}\n
\n {\n dragging.current = true;\n dragStartFrame.current = frame;\n setInteracting(true);\n }}\n onInput={(event) => {\n dragging.current = true;\n const next = Number(event.currentTarget.value);\n setFrame(next);\n presenter.setFrame(next);\n }}\n onPointerUp={(event) => {\n setInteracting(false);\n void commit(Number(event.currentTarget.value));\n }}\n onPointerCancel={() => {\n setInteracting(false);\n dragging.current = false;\n setFrame(dragStartFrame.current);\n presenter.setFrame(dragStartFrame.current);\n }}\n onKeyUp={(event) => {\n setInteracting(false);\n if (event.key !== \"Escape\") void commit(Number(event.currentTarget.value));\n }}\n onBlur={(event) => {\n setInteracting(false);\n if (dragging.current) void commit(Number(event.currentTarget.value));\n }}\n onKeyDown={(event) => {\n setInteracting(true);\n if (event.key === \"Escape\" && !pending) {\n setInteracting(false);\n dragging.current = false;\n setFrame(dragStartFrame.current);\n presenter.setFrame(dragStartFrame.current);\n }\n }}\n />\n \n );\n}\n\ntype AppearanceChoice = NativeThemeId | \"system\" | \"liang\";\n\nconst APPEARANCE_CHOICES: readonly { id: AppearanceChoice; labelKey: string }[] = [\n { id: \"light\", labelKey: \"appearance.light\" },\n { id: \"dark\", labelKey: \"appearance.dark\" },\n { id: \"system\", labelKey: \"appearance.system\" },\n { id: \"liang\", labelKey: \"appearance.liang\" },\n];\n\nfunction NativeAppearanceIcon({ id }: { id: Exclude }) {\n if (id === \"light\") {\n return (\n \n \n \n \n \n \n \n \n \n \n \n );\n }\n\n if (id === \"dark\") {\n return (\n \n \n \n );\n }\n\n return (\n \n \n \n \n );\n}\n\nfunction AppearanceSkinRow({ scope, presenter, theme, subscribeTheme, t }: SettingsRowProps) {\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n );\n const preference = useSyncExternalStore(\n subscribeTheme,\n () => theme.getTheme().preference,\n );\n const selected = snapshot.enabled\n ? \"liang\"\n : preference === \"light\" || preference === \"dark\" || preference === \"system\"\n ? preference\n : \"system\";\n const [pending, setPending] = useState(false);\n\n const choose = async (next: AppearanceChoice) => {\n if (next === selected || pending) return;\n setPending(true);\n try {\n if (next === \"liang\") {\n await presenter.choose(true);\n } else {\n // Disable the custom skin first. This is intentionally unconditional:\n // it makes the native choice the only writer after this click, even if\n // the external preference store is one render behind.\n await presenter.choose(false);\n theme.setTheme(next);\n }\n } finally {\n setPending(false);\n }\n };\n\n return (\n
\n {t(\"appearance.title\")}\n
\n {APPEARANCE_CHOICES.map(({ id, labelKey }) => (\n void choose(id)}\n key={id}\n >\n {id === \"liang\" ? (\n \u25C8\n ) : (\n \n )}\n {t(labelKey)}\n \n ))}\n
\n
\n );\n}\n\nconst NATIVE_APPEARANCE_GROUP = '[class*=\"_8HJdBW_group\"]';\nconst NATIVE_APPEARANCE_ROW = '[class*=\"_8HJdBW_cubeRow\"]';\nconst LIANG_APPEARANCE_BUTTON = \"liang-appearance-choice\";\nconst LIANG_BINDING_CONTROL = \"liang-appearance-binding\";\nconst LIANG_BINDING_INPUT = \"liang-appearance-binding__input\";\n\nfunction installLiangAppearanceButton(scope: PreferenceStore, presenter: SkinPresenter) {\n let pending = false;\n const hookedNativeButtons = new Set();\n const nativeClickHandlers = new Map void>();\n\n const sync = () => {\n const group = [...document.querySelectorAll(NATIVE_APPEARANCE_GROUP)]\n .find((node) => node.querySelector('[class*=\"_8HJdBW_themeCube\"]'));\n const row = group?.querySelector(NATIVE_APPEARANCE_ROW);\n if (row === undefined || row === null) return;\n\n let customButton = row.querySelector(`.${LIANG_APPEARANCE_BUTTON}`);\n if (customButton === null) {\n customButton = document.createElement(\"button\");\n customButton.className = LIANG_APPEARANCE_BUTTON;\n customButton.type = \"button\";\n customButton.dataset.plugin = PACKAGE_ID;\n customButton.setAttribute(\"aria-label\", \"\u6ED1\u52A8\u53D8\u7956\");\n\n const icon = document.createElement(\"span\");\n icon.className = \"liang-appearance-choice__icon\";\n icon.setAttribute(\"aria-hidden\", \"true\");\n icon.textContent = \"\u25C8\";\n\n const label = document.createElement(\"span\");\n label.className = \"liang-appearance-choice__label\";\n label.textContent = document.documentElement.lang.toLowerCase().startsWith(\"en\")\n ? \"Slider\"\n : \"\u6ED1\u52A8\u53D8\u7956\";\n customButton.append(icon, label);\n customButton.addEventListener(\"click\", () => {\n if (pending || scope.getSnapshot().enabled) return;\n pending = true;\n sync();\n void presenter.choose(true).finally(() => {\n pending = false;\n sync();\n });\n });\n }\n\n if (customButton.parentElement !== row) row.append(customButton);\n const snapshot = scope.getSnapshot();\n customButton.disabled = pending;\n customButton.setAttribute(\"aria-pressed\", String(snapshot.enabled));\n\n let bindingControl = group.querySelector(`.${LIANG_BINDING_CONTROL}`);\n if (!snapshot.enabled) {\n bindingControl?.remove();\n bindingControl = null;\n } else if (bindingControl === null) {\n bindingControl = document.createElement(\"label\");\n bindingControl.className = LIANG_BINDING_CONTROL;\n bindingControl.dataset.plugin = PACKAGE_ID;\n\n const bindingCopy = document.createElement(\"span\");\n bindingCopy.className = \"liang-appearance-binding__copy\";\n\n const bindingLabel = document.createElement(\"span\");\n bindingLabel.className = \"liang-appearance-binding__label\";\n const bindingText = document.documentElement.lang.toLowerCase().startsWith(\"en\")\n ? \"Bind slider to reasoning level\"\n : \"\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7\";\n bindingLabel.textContent = bindingText;\n\n const bindingDescription = document.createElement(\"span\");\n bindingDescription.className = \"liang-appearance-binding__description\";\n bindingDescription.id = \"liang-appearance-binding-description\";\n bindingDescription.textContent = document.documentElement.lang.toLowerCase().startsWith(\"en\")\n ? \"When off, the slider does not change the reasoning level.\"\n : \"\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7\";\n bindingCopy.append(bindingLabel, bindingDescription);\n\n const bindingInput = document.createElement(\"input\");\n bindingInput.className = LIANG_BINDING_INPUT;\n bindingInput.type = \"checkbox\";\n bindingInput.setAttribute(\"role\", \"switch\");\n bindingInput.setAttribute(\"aria-label\", bindingText);\n bindingInput.setAttribute(\"aria-describedby\", bindingDescription.id);\n bindingInput.addEventListener(\"change\", () => {\n void scope.setBindEffort(bindingInput.checked).catch(() => sync());\n });\n\n bindingControl.append(bindingCopy, bindingInput);\n group.append(bindingControl);\n }\n\n if (bindingControl !== null) {\n const bindingInput = bindingControl.querySelector(`.${LIANG_BINDING_INPUT}`);\n if (bindingInput !== null) {\n bindingInput.checked = snapshot.bindEffort;\n bindingInput.setAttribute(\"aria-checked\", String(snapshot.bindEffort));\n bindingInput.disabled = pending;\n }\n }\n\n for (const nativeButton of row.querySelectorAll('[class*=\"_8HJdBW_themeCube\"]')) {\n if (hookedNativeButtons.has(nativeButton)) continue;\n hookedNativeButtons.add(nativeButton);\n const handleNativeClick = () => {\n if (pending || !scope.getSnapshot().enabled) return;\n pending = true;\n sync();\n void presenter.choose(false).finally(() => {\n pending = false;\n sync();\n });\n };\n nativeClickHandlers.set(nativeButton, handleNativeClick);\n nativeButton.addEventListener(\"click\", handleNativeClick, { capture: true });\n }\n };\n\n const observer = new MutationObserver(sync);\n observer.observe(document.body, { childList: true, subtree: true });\n const unsubscribe = scope.subscribe(sync);\n sync();\n\n return () => {\n observer.disconnect();\n unsubscribe();\n for (const [nativeButton, handleNativeClick] of nativeClickHandlers) {\n nativeButton.removeEventListener(\"click\", handleNativeClick, { capture: true });\n }\n document.querySelectorAll(`.${LIANG_APPEARANCE_BUTTON}`).forEach((button) => button.remove());\n document.querySelectorAll(`.${LIANG_BINDING_CONTROL}`).forEach((control) => control.remove());\n };\n}\n\nexport const inject = [\n \"slots\",\n \"sessions\",\n \"modelDirectories\",\n \"locale\",\n \"theme\",\n];\n\nfunction createPreferenceStore(): PreferenceStore {\n // The market's active skin is the source of truth for whether this client\n // should be visible. The appearance switch is therefore scoped to this\n // client activation and must not survive switching away and back.\n try {\n localStorage.removeItem(\"dsh-liang-intensity-skin.enabled\");\n } catch {\n // Storage may be unavailable; the in-memory default still enables Liang.\n }\n let snapshot: SkinSettings = {\n enabled: true,\n bindEffort: localStorage.getItem(BIND_EFFORT_KEY) !== \"0\",\n };\n const listeners = new Set<() => void>();\n const onStorage = (event: StorageEvent) => {\n if (event.key !== BIND_EFFORT_KEY) return;\n const next = {\n enabled: snapshot.enabled,\n bindEffort: event.newValue !== \"0\",\n };\n if (next.enabled === snapshot.enabled && next.bindEffort === snapshot.bindEffort) return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n window.addEventListener(\"storage\", onStorage);\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async set(enabled) {\n if (enabled === snapshot.enabled) return;\n snapshot = { ...snapshot, enabled };\n for (const listener of listeners) listener();\n },\n async setBindEffort(bindEffort) {\n if (bindEffort === snapshot.bindEffort) return;\n localStorage.setItem(BIND_EFFORT_KEY, bindEffort ? \"1\" : \"0\");\n snapshot = { ...snapshot, bindEffort };\n for (const listener of listeners) listener();\n },\n dispose() {\n window.removeEventListener(\"storage\", onStorage);\n listeners.clear();\n },\n };\n}\n\nexport function apply(ctx: ClientContext) {\n const style = document.createElement(\"style\");\n style.dataset.plugin = PACKAGE_ID;\n style.textContent = styles;\n document.head.append(style);\n ctx.effect(() => () => style.remove(), \"liang-intensity-skin: scoped styles\");\n\n const scope = createPreferenceStore();\n ctx.effect(() => () => scope.dispose(), \"liang-intensity-skin: appearance preference\");\n const theme = ctx.get(\"theme\") as ThemeService;\n const presenter = new SkinPresenter(scope, theme);\n ctx.effect(() => () => presenter.dispose(), \"liang-intensity-skin: backdrop presenter\");\n ctx.effect(\n () => installLiangAppearanceButton(scope, presenter),\n \"liang-intensity-skin: native appearance extension\",\n );\n ctx.effect(\n () => ctx.locale.register(LOCALE_NAMESPACE, {\n zh: {\n \"appearance.title\": \"\u5916\u89C2\",\n \"appearance.light\": \"\u6D45\u8272\",\n \"appearance.dark\": \"\u6DF1\u8272\",\n \"appearance.system\": \"\u8DDF\u968F\u7CFB\u7EDF\",\n \"appearance.liang\": \"\u6ED1\u52A8\u53D8\u7956\",\n },\n en: {\n \"appearance.title\": \"Appearance\",\n \"appearance.light\": \"Light\",\n \"appearance.dark\": \"Dark\",\n \"appearance.system\": \"System\",\n \"appearance.liang\": \"Slider\",\n },\n }), \"liang-intensity-skin: settings locale\");\n\n ctx.slots.inject(\"conversation.input.right\", () => ctx.slots.register({\n name: \"conversation.input.right\",\n id: \"liang-intensity-control\",\n order: 10,\n inject: (sessionId: string) => {\n const available = ctx.sessions.subagentAddress(sessionId) === undefined;\n const directory = ctx.modelDirectories.directoryFor(sessionId) as ModelDirectory;\n return {\n directory: directory.store,\n presenter,\n scope,\n load: () => {\n if (available) void directory.load().catch(() => undefined);\n },\n select: (selection: ModelSelection) => available\n ? directory.select(selection).then((result) => result.ok, () => false)\n : Promise.resolve(false),\n };\n },\n }, LiangEffortSlider));\n}\n", ".liang-skin-backdrop {\n position: fixed;\n z-index: 0;\n inset: 0;\n overflow: hidden;\n pointer-events: none;\n background: var(--liang-page, transparent);\n opacity: 0;\n transition: opacity 220ms ease, background-color 180ms linear;\n}\n\nbody[data-liang-skin=\"on\"] .liang-skin-backdrop {\n opacity: 1;\n}\n\n.liang-skin-backdrop img {\n position: absolute;\n top: 50%;\n right: 0;\n display: block;\n width: min(42vw, 720px);\n height: 100vh;\n object-fit: cover;\n object-position: 50% 42%;\n opacity: var(--liang-portrait-opacity, 0.45);\n transform: translateY(-50%);\n filter: none;\n mask-image: linear-gradient(90deg, transparent 0%, rgb(0 0 0 / 20%) 10%, black 30%, black 100%);\n transition: filter 180ms linear;\n}\n\n.liang-skin-backdrop img {\n display: none;\n}\n\n.liang-skin-backdrop[data-media=\"sequence\"] .liang-skin-sequence-frame {\n display: block;\n opacity: var(--liang-portrait-opacity, 0.82);\n}\n\n.liang-skin-backdrop[data-media=\"color\"] img {\n display: none;\n}\n\n.liang-skin-backdrop::after {\n position: absolute;\n inset: 0;\n content: \"\";\n background: linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%);\n}\n\nbody[data-liang-skin=\"on\"] {\n --dsw-alias-bg-base: var(--liang-bg-base) !important;\n --dsw-alias-bg-layer-1: var(--liang-layer-1) !important;\n --dsw-alias-bg-layer-2: var(--liang-layer-2) !important;\n --dsw-alias-bg-layer-3: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-fill: var(--liang-sidebar) !important;\n --dsw-alias-label-primary: var(--liang-ink) !important;\n --dsw-alias-label-primary-dimmed: var(--liang-ink) !important;\n --dsw-alias-label-secondary: var(--liang-secondary) !important;\n --dsw-alias-label-tertiary: var(--liang-tertiary) !important;\n --dsw-alias-label-caption: var(--liang-tertiary) !important;\n --dsw-alias-border-l1: var(--liang-border) !important;\n --dsw-alias-border-l2-darkmode-thin: var(--liang-border) !important;\n --dsw-alias-border-l2: var(--liang-border) !important;\n --dsw-alias-border-l3: var(--liang-border) !important;\n --dsw-alias-button-primary-fill: var(--liang-accent) !important;\n --dsw-alias-button-primary-hover: var(--liang-accent-hover) !important;\n --dsw-alias-button-info-fill: var(--liang-accent) !important;\n --dsw-alias-button-info-hover: var(--liang-accent-hover) !important;\n --dsw-alias-state-business-primary: var(--liang-accent) !important;\n --dsw-alias-interactive-bg-hover: var(--liang-hover) !important;\n --dsw-alias-interactive-bg-active: var(--liang-hover) !important;\n --dsw-alias-button-elevated-fill: var(--liang-layer-1) !important;\n --dsw-alias-button-floating-fill: var(--liang-layer-2) !important;\n --dsw-alias-button-floating-hover: var(--liang-layer-3) !important;\n --dsw-alias-interactive-bg-hover-solid: var(--liang-layer-2) !important;\n --dsw-specific-bubble: var(--liang-layer-2) !important;\n --dsw-specific-input-major: var(--liang-layer-1) !important;\n --dsw-specific-menu: var(--liang-layer-3) !important;\n --dsw-specific-selector: var(--liang-layer-2) !important;\n --dsw-alias-markdown-citation: var(--liang-layer-2) !important;\n --dsw-alias-markdown-code-block-banner: var(--liang-layer-1) !important;\n --dsw-alias-markdown-code-block: var(--liang-layer-1) !important;\n --dsw-alias-markdown-code-segment-selected: var(--liang-layer-2) !important;\n --dsw-alias-markdown-code-segment-unselected: var(--liang-layer-1) !important;\n --dsw-alias-markdown-inline-code: var(--liang-layer-2) !important;\n --dsw-alias-markdown-placeholder: var(--liang-layer-1) !important;\n --dsw-alias-markdown-tag: var(--liang-layer-2) !important;\n background: var(--liang-page) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) {\n --dsw-alias-brand-primary-new-colorprimary-new-color: rgb(65 118 230) !important;\n --dsw-alias-brand-primary: rgb(15 17 21) !important;\n --dsw-alias-brand-text: rgb(15 17 21) !important;\n --dsw-alias-button-primary-fill: rgb(15 17 21) !important;\n --dsw-alias-button-primary-hover: rgb(53 54 56) !important;\n --dsw-alias-button-info-fill: rgb(65 118 230) !important;\n --dsw-alias-button-info-hover: rgb(103 158 254) !important;\n --dsw-alias-state-business-primary: rgb(65 118 230) !important;\n --dsw-alias-label-primary-inverted: rgb(249 250 251) !important;\n --dsw-alias-button-elevated-fill: rgb(255 255 255) !important;\n --dsw-alias-button-floating-fill: rgb(255 255 255) !important;\n --dsw-alias-button-floating-hover: rgb(241 243 245) !important;\n --dsw-alias-interactive-bg-hover-solid: rgb(241 243 245) !important;\n --dsw-specific-bubble: rgb(237 243 254) !important;\n --dsw-specific-input-major: rgb(255 255 255) !important;\n --dsw-specific-menu: rgb(255 255 255) !important;\n --dsw-specific-selector: rgb(245 246 247) !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] {\n --dsw-alias-brand-primary-new-colorprimary-new-color: #c19a49 !important;\n --dsw-alias-brand-primary: #c19a49 !important;\n --dsw-alias-brand-text: #d5b56e !important;\n --dsw-alias-button-info-fill: #c19a49 !important;\n --dsw-alias-button-info-hover: #d5b56e !important;\n --dsw-alias-state-business-primary: #c19a49 !important;\n --dsw-alias-bg-module-platform: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-nav-item-active: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-nav-item-hover: var(--liang-hover) !important;\n}\n\n/* The host renders the running-turn label as a clipped blue text gradient.\n Retarget only that gradient's color tokens in the custom dark skin; keep\n the host's text clipping and shimmer behavior, and leave light mode alone. */\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"_turnStatus\"] {\n --dsw-static-deepseek-500: #c19a49 !important;\n --dsw-static-deepseek-200: #d5b56e !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"_turnStatusClock\"] {\n color: #c19a49 !important;\n -webkit-text-fill-color: #c19a49 !important;\n}\n\n/* Preserve the wordmark badge contrast in the low-intensity shell. */\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > rect[width=\"52\"] {\n fill: rgb(15 17 21) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > g[clip-path*=\"badge\"] path {\n fill: rgb(249 250 251) !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > rect[width=\"52\"] {\n fill: #c19a49 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > g[clip-path*=\"badge\"] path {\n fill: #171816 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [aria-label^=\"\u9009\u62E9\u6A21\u578B\"] > span:nth-of-type(2) {\n color: #d5b56e !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"heroGlow\"] ellipse {\n fill: #c19a49 !important;\n fill-opacity: 0.16 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] span[class*=\"_previewBadge\"] {\n color: #171816 !important;\n background: #c19a49 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u53D1\u9001\u6D88\u606F\"] {\n color: #171816 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u53D1\u9001\u6D88\u606F\"]:disabled {\n opacity: 0.62;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u53D1\u9001\u6D88\u606F\"] {\n color: #fff !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u53D1\u9001\u6D88\u606F\"]:disabled {\n color: rgb(255 255 255 / 92%) !important;\n background: rgb(65 118 230 / 42%) !important;\n opacity: 1;\n}\n\nbody[data-liang-skin=\"on\"] > #root {\n position: relative;\n z-index: 1;\n background: transparent !important;\n}\n\n/* Windows paints the sidebar fill across the host frame, covering the portrait.\n Match macOS's transparent frame while keeping the sidebar, caption, and\n conversation surfaces painted by their own host rules. */\nhtml[data-windows-titlebar] body[data-liang-skin=\"on\"] #root [class*=\"_frame\"]:has(> [class*=\"_centerCol\"]) {\n background: transparent !important;\n}\n\n/* macOS adds different translucent sidebar materials for native light/dark.\n While Liang is active, the effort palette owns the sidebar fill. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_frame\"]:has(> [class*=\"_centerCol\"]) > [class*=\"_sidebarCol\"] {\n background: var(--liang-sidebar) !important;\n}\n\n/* Keep the host's expanded new-session surface on the same palette. The\n collapsed icon keeps its native transparent surface and hover behavior. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"]:has(> [class*=\"_logoRow\"]):not([class*=\"_collapsed\"]) button[class*=\"_newSession\"] {\n background: var(--dsw-alias-button-elevated-fill) !important;\n}\n\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"]:has(> [class*=\"_logoRow\"]):not([class*=\"_collapsed\"]) button[class*=\"_newSession\"]:hover {\n background: var(--dsw-alias-button-floating-hover) !important;\n}\n\n/* The native dark rule also outranks the collapsed icon's transparent fill. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"][class*=\"_collapsed\"]:has(> [class*=\"_logoRow\"]) button[class*=\"_newSession\"] {\n background: transparent !important;\n}\n\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"][class*=\"_collapsed\"]:has(> [class*=\"_logoRow\"]) button[class*=\"_newSession\"]:hover {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\nbody[data-liang-skin=\"on\"] button,\nbody[data-liang-skin=\"on\"] input,\nbody[data-liang-skin=\"on\"] textarea {\n transition: color 160ms linear, background-color 160ms linear, border-color 160ms linear;\n}\n\n/* The composer card has a native theme-mode fill, so keep its surface and\n readable ink aligned with the active Liang palette. */\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] {\n border-color: var(--liang-border) !important;\n color: var(--liang-ink) !important;\n background: var(--liang-layer-1) !important;\n box-shadow: 0 8px 28px rgb(20 22 20 / 8%);\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] :is(textarea, button, [data-input-mirror=\"true\"]) {\n color: var(--liang-ink) !important;\n caret-color: var(--liang-accent) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] textarea::placeholder {\n color: var(--liang-tertiary) !important;\n opacity: 1;\n}\n\n.liang-effort-control {\n --liang-control-accent: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6);\n --liang-control-rail: var(--dsw-alias-border-l3, rgb(0 0 0 / 16%));\n position: relative;\n display: flex;\n width: 124px;\n height: 28px;\n align-items: center;\n margin: 0 3px;\n overflow: visible;\n}\n\nbody[data-liang-skin=\"on\"] .liang-effort-control {\n --liang-control-accent: var(--liang-secondary);\n --liang-control-rail: color-mix(in srgb, var(--liang-secondary) 28%, transparent);\n}\n\n.liang-effort-control__ticks {\n position: absolute;\n z-index: 0;\n inset: 0 10px;\n pointer-events: none;\n}\n\n.liang-effort-control__ticks::before {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: 1px;\n content: \"\";\n background: var(--liang-control-rail);\n transform: translateY(-50%);\n}\n\n.liang-effort-control__tooltip {\n position: absolute;\n z-index: 4;\n bottom: calc(100% + 7px);\n left: calc(10px + (100% - 20px) * var(--liang-slider-ratio));\n min-width: max-content;\n padding: 5px 8px;\n border: 1px solid var(--liang-border, rgb(0 0 0 / 12%));\n border-radius: 7px;\n color: var(--liang-ink, #171816);\n background: var(--liang-layer-1, #fff);\n box-shadow: 0 5px 16px rgb(0 0 0 / 16%);\n font-size: 11px;\n font-weight: 600;\n line-height: 16px;\n letter-spacing: 0.01em;\n pointer-events: none;\n transform: translateX(-50%);\n white-space: nowrap;\n}\n\n.liang-effort-control__tooltip::after {\n position: absolute;\n top: 100%;\n left: 50%;\n width: 7px;\n height: 7px;\n content: \"\";\n background: inherit;\n transform: translate(-50%, -4px) rotate(45deg);\n}\n\n.liang-effort-control__tick {\n position: absolute;\n top: 50%;\n width: 1px;\n height: 7px;\n background: var(--liang-control-accent);\n opacity: 0.42;\n transform: translate(-50%, -50%);\n}\n\n.liang-effort-control__range {\n position: relative;\n z-index: 1;\n width: calc(100% - 7px);\n height: 28px;\n margin: 0 3.5px;\n cursor: ew-resize;\n appearance: none;\n background: transparent;\n touch-action: pan-y;\n}\n\n.liang-effort-control__range:disabled {\n cursor: progress;\n opacity: 0.58;\n}\n\n.liang-effort-control__range::-webkit-slider-runnable-track {\n height: 1px;\n border-radius: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-webkit-slider-thumb {\n width: 13px;\n height: 13px;\n margin-top: -6px;\n border: 1.5px solid color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n border-radius: 50%;\n appearance: none;\n background: radial-gradient(circle, var(--liang-control-accent) 0 2px, color-mix(in srgb, var(--liang-control-accent) 22%, var(--dsw-alias-bg-layer-1, #fff)) 2.5px);\n box-shadow: 0 1px 4px rgb(0 0 0 / 14%);\n}\n\nbody[data-liang-skin=\"on\"] .liang-effort-control__range::-webkit-slider-thumb {\n width: 13px;\n height: 13px;\n margin-top: -6px;\n border-color: color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n background: radial-gradient(circle, var(--liang-control-accent) 0 2px, color-mix(in srgb, var(--liang-control-accent) 22%, var(--liang-layer-1)) 2.5px);\n}\n\n.liang-effort-control__range::-moz-range-track {\n height: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-moz-range-progress {\n height: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-moz-range-thumb {\n width: 11px;\n height: 11px;\n border: 1.5px solid color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n border-radius: 50%;\n background: color-mix(in srgb, var(--liang-control-accent) 24%, var(--dsw-alias-bg-layer-1, #fff));\n}\n\n.liang-effort-control__range:focus-visible {\n outline: none;\n}\n\n.liang-effort-control[data-state=\"error\"] {\n --liang-control-accent: var(--dsw-alias-state-error-primary, #e43c3c);\n}\n\n.liang-appearance-choice {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n flex: 180px;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: 4px;\n padding: 20px 32px;\n border-radius: 16px;\n font: inherit;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: transparent;\n font-size: 14px;\n line-height: 22px;\n display: flex;\n}\n\n[class*=\"_8HJdBW_cubeRow\"]:has(.liang-appearance-choice) {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n}\n\n.liang-appearance-choice:hover:not([aria-pressed=\"true\"]) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.liang-appearance-choice[aria-pressed=\"true\"] {\n border-color: var(--dsw-static-neutral-bluish-400);\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.liang-appearance-choice__icon {\n display: block;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n color: currentColor;\n font-size: 16px;\n line-height: 16px;\n}\n\n.liang-appearance-choice__label {\n display: block;\n}\n\n/* Keep the host's three theme cubes untouched. These rules only style the\n fourth button that is appended to the host cube row. */\n.liang-appearance-choice:disabled {\n cursor: progress;\n opacity: 0.7;\n}\n\n.liang-appearance-binding {\n box-sizing: border-box;\n display: flex;\n width: 100%;\n min-height: 28px;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 4px 0 0;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n}\n\n.liang-appearance-binding__label {\n display: block;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.liang-appearance-binding__copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 1px;\n}\n\n.liang-appearance-binding__description {\n display: block;\n overflow: hidden;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.liang-appearance-binding__input {\n position: relative;\n flex: 0 0 36px;\n width: 36px;\n height: 20px;\n margin: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n outline: none;\n cursor: pointer;\n appearance: none;\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-module-platform));\n}\n\n.liang-appearance-binding__input::before {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n content: \"\";\n background: var(--dsw-alias-label-primary);\n box-shadow: 0 1px 3px rgb(0 0 0 / 18%);\n transition: transform 160ms ease, background-color 160ms ease;\n}\n\n.liang-appearance-binding__input:checked {\n border-color: var(--dsw-static-neutral-bluish-400);\n background: var(--dsw-static-neutral-bluish-400);\n}\n\n.liang-appearance-binding__input:checked::before {\n background: var(--dsw-alias-bg-layer-1, #fff);\n transform: translateX(16px);\n}\n\n.liang-appearance-binding__input:focus-visible {\n box-shadow: 0 0 0 2px var(--dsw-alias-interactive-bg-hover);\n}\n\n.liang-appearance-binding__input:disabled {\n cursor: progress;\n opacity: 0.7;\n}\n\n@media (max-width: 760px) {\n .liang-effort-control {\n width: 92px;\n }\n\n .liang-skin-backdrop img {\n right: -18vw;\n width: 92vw;\n opacity: calc(var(--liang-portrait-opacity, 0.45) * 0.72);\n }\n\n}\n\n@media (prefers-reduced-motion: reduce) {\n .liang-skin-backdrop,\n .liang-skin-backdrop img,\n body[data-liang-skin=\"on\"] button,\n body[data-liang-skin=\"on\"] input,\n body[data-liang-skin=\"on\"] textarea {\n transition: none;\n }\n}\n", "export const PREVIEW_MAX_FRAME = 240;\nexport const MAX_LEVEL = 30;\n\nconst LIANG_RANKS = [\"\u5C0F\u96BE\u6881\", \"\u7262\u6881\", \"\u6881\u5B50\", \"\u6881\u5723\", \"\u6881\u795E\", \"\u6881\u7956\"] as const;\n\nexport interface EffortLike {\n id: string;\n name: string;\n description?: string;\n}\n\nexport interface Palette {\n level: number;\n stage: number;\n strength: number;\n page: string;\n base: string;\n layer1: string;\n layer2: string;\n layer3: string;\n sidebar: string;\n ink: string;\n secondary: string;\n tertiary: string;\n border: string;\n accent: string;\n accentHover: string;\n hover: string;\n portraitOpacity: string;\n}\n\ntype Rgb = readonly [number, number, number];\n\ninterface PaletteStop {\n at: number;\n page: Rgb;\n surface: Rgb;\n surface2: Rgb;\n ink: Rgb;\n secondary: Rgb;\n accent: Rgb;\n portraitOpacity: number;\n}\n\n// These are the original calibrator's six stage anchors: pale neutral at the\n// low end, progressively smoked surfaces, then charcoal and antique gold.\nconst STOPS: readonly PaletteStop[] = [\n { at: 0, page: [232, 233, 229], surface: [248, 248, 245], surface2: [238, 239, 235], ink: [23, 24, 22], secondary: [112, 116, 111], accent: [181, 43, 36], portraitOpacity: 0.92 },\n { at: 6, page: [211, 211, 206], surface: [239, 239, 234], surface2: [224, 224, 218], ink: [26, 26, 24], secondary: [101, 101, 96], accent: [181, 43, 36], portraitOpacity: 0.93 },\n { at: 12, page: [171, 168, 162], surface: [211, 208, 201], surface2: [190, 186, 179], ink: [27, 26, 24], secondary: [83, 79, 74], accent: [166, 54, 42], portraitOpacity: 0.94 },\n { at: 18, page: [117, 112, 106], surface: [154, 148, 140], surface2: [128, 122, 115], ink: [24, 22, 20], secondary: [65, 61, 56], accent: [154, 56, 42], portraitOpacity: 0.94 },\n { at: 24, page: [43, 39, 37], surface: [48, 43, 40], surface2: [58, 51, 46], ink: [244, 241, 232], secondary: [184, 180, 169], accent: [181, 92, 54], portraitOpacity: 0.94 },\n { at: 30, page: [17, 17, 17], surface: [30, 27, 25], surface2: [41, 35, 30], ink: [244, 241, 232], secondary: [184, 180, 169], accent: [193, 154, 73], portraitOpacity: 0.94 },\n];\n\nexport function clampFrame(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(value)));\n}\n\nexport function frameForEffort(index: number, count: number): number {\n if (count <= 1 || !Number.isFinite(index)) return 0;\n const safe = Math.min(count - 1, Math.max(0, Math.round(index)));\n return Math.round((safe / (count - 1)) * PREVIEW_MAX_FRAME);\n}\n\nexport function nearestEffortIndex(frame: number, efforts: readonly EffortLike[]): number {\n if (efforts.length === 0) return -1;\n const safe = clampFrame(frame);\n let best = 0;\n let distance = Math.abs(safe - frameForEffort(0, efforts.length));\n for (let index = 1; index < efforts.length; index += 1) {\n const next = Math.abs(safe - frameForEffort(index, efforts.length));\n if (next < distance) {\n best = index;\n distance = next;\n }\n }\n return best;\n}\n\nexport function selectedEffortIndex(\n efforts: readonly EffortLike[],\n selectedId?: string,\n defaultId?: string,\n): number {\n const id = selectedId ?? defaultId;\n return id === undefined ? -1 : efforts.findIndex((effort) => effort.id === id);\n}\n\nexport function portraitBlendForLevel(level: number, anchors: readonly number[]) {\n if (anchors.length === 0) return { lowerIndex: -1, upperIndex: -1, mix: 0 };\n const safeLevel = Number.isFinite(level)\n ? Math.min(anchors[anchors.length - 1], Math.max(anchors[0], level))\n : anchors[0];\n let upperIndex = anchors.findIndex((anchor) => anchor >= safeLevel);\n if (upperIndex < 0) upperIndex = anchors.length - 1;\n const lowerIndex = Math.max(0, upperIndex - (anchors[upperIndex] > safeLevel ? 1 : 0));\n const span = anchors[upperIndex] - anchors[lowerIndex];\n return {\n lowerIndex,\n upperIndex,\n mix: span === 0 ? 0 : (safeLevel - anchors[lowerIndex]) / span,\n };\n}\n\nexport function liangRankForFrame(rawFrame: number): string {\n const level = (clampFrame(rawFrame) / PREVIEW_MAX_FRAME) * MAX_LEVEL;\n const index = level >= MAX_LEVEL ? LIANG_RANKS.length - 1 : Math.floor(level / 6);\n return LIANG_RANKS[Math.min(LIANG_RANKS.length - 1, Math.max(0, index))];\n}\n\nexport function indicatorLabel(rawFrame: number, efforts: readonly EffortLike[]): string {\n const effort = efforts[nearestEffortIndex(rawFrame, efforts)];\n return effort === undefined\n ? liangRankForFrame(rawFrame)\n : `${liangRankForFrame(rawFrame)} \u00B7 ${effort.name}`;\n}\n\nfunction lerp(a: number, b: number, amount: number): number {\n return a + (b - a) * amount;\n}\n\nfunction mix(a: Rgb, b: Rgb, amount: number): Rgb {\n return [\n Math.round(lerp(a[0], b[0], amount)),\n Math.round(lerp(a[1], b[1], amount)),\n Math.round(lerp(a[2], b[2], amount)),\n ];\n}\n\nfunction rgb(value: Rgb, alpha = 1): string {\n return alpha === 1\n ? `rgb(${value[0]} ${value[1]} ${value[2]})`\n : `rgb(${value[0]} ${value[1]} ${value[2]} / ${alpha})`;\n}\n\nexport function paletteForFrame(rawFrame: number): Palette {\n const frame = clampFrame(rawFrame);\n const level = (frame / PREVIEW_MAX_FRAME) * MAX_LEVEL;\n const portraitStage = Math.min(5, Math.floor(level / 6));\n const portraitFrom = STOPS[portraitStage];\n const portraitTo = STOPS[Math.min(5, portraitStage + 1)];\n const portraitAmount = portraitFrom === portraitTo\n ? 0\n : (level - portraitFrom.at) / (portraitTo.at - portraitFrom.at);\n\n // The UI itself is intentionally binary: levels 0\u201323 keep the native light\n // shell; entering the original \u6881\u795E/\u6881\u7956 region at level 24 switches black/gold.\n const dark = level >= 24;\n const stage = dark ? 5 : 0;\n const ui = dark ? STOPS[5] : STOPS[0];\n const page = ui.page;\n const surface = ui.surface;\n const surface2 = ui.surface2;\n const sidebar = mix(page, surface2, 0.25);\n const ink = ui.ink;\n const secondary = ui.secondary;\n // Keep Harness' native blue in the light shell. The original calibrator's\n // antique gold only becomes the product accent once the dark region begins.\n const accent: Rgb = dark ? [193, 154, 73] : [65, 118, 230];\n const accentHover = mix(accent, ink, 0.13);\n\n return {\n level,\n stage,\n strength: level / MAX_LEVEL,\n page: rgb(page),\n // Keep the shell readable while allowing the right-side portrait to remain\n // visibly present. Dense controls use the opaque layer tokens below.\n base: rgb(page, dark ? 0.42 : 0.28),\n layer1: rgb(surface, 0.94),\n layer2: rgb(surface2, 0.96),\n layer3: rgb(surface2, 0.99),\n sidebar: rgb(sidebar, 0.96),\n ink: rgb(ink),\n secondary: rgb(secondary),\n tertiary: rgb(mix(secondary, page, 0.28)),\n border: rgb(ink, dark ? 0.15 : 0.12),\n accent: rgb(accent),\n accentHover: rgb(accentHover),\n hover: rgb(ink, dark ? 0.09 : 0.07),\n portraitOpacity: String(lerp(\n portraitFrom.portraitOpacity,\n portraitTo.portraitOpacity,\n portraitAmount,\n )),\n };\n}\n"], - "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAMO;;;ACNP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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", - "names": ["mix"] + "sources": ["../src/client/index.tsx", "../src/client/skin.css", "../src/client/appearance.ts", "../src/client/portrait-loader.ts", "../src/client/logic.ts"], + "sourcesContent": ["import {\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport type { Context as ClientContext } from \"@deepseek-ai/cordis\";\nimport type { SnapshotStore } from \"@deepseek-ai/dsh-client-store\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-conversation/client\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-settings/client\";\nimport type {} from \"@deepseek-ai/dsh-client-ui-model-selection/client\";\nimport styles from \"./skin.css\";\nimport { installLiangAppearanceButton } from \"./appearance\";\nimport { createPortraitLoader } from \"./portrait-loader\";\nimport {\n PREVIEW_MAX_FRAME,\n frameForEffort,\n indicatorLabel,\n nearestEffortIndex,\n paletteForFrame,\n selectedEffortIndex,\n type EffortLike,\n} from \"./logic\";\n\nconst PACKAGE_ID = \"dsh-client-liang-intensity-skin\";\nconst LOCALE_NAMESPACE = \"liang.skin\";\nconst ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`;\nconst BIND_EFFORT_KEY = \"dsh-liang-intensity-skin.bind-effort\";\n\nconst PORTRAIT_ANCHORS = [\n { level: 0, file: \"stage-00.webp\" },\n { level: 1, file: \"level-01.webp\" },\n { level: 3, file: \"level-03.webp\" },\n { level: 4, file: \"level-04.webp\" },\n { level: 6, file: \"stage-06.webp\" },\n { level: 7, file: \"level-07.webp\" },\n { level: 9, file: \"level-09.webp\" },\n { level: 10, file: \"level-10.webp\" },\n { level: 12, file: \"stage-12.webp\" },\n { level: 13, file: \"level-13.webp\" },\n { level: 14, file: \"level-14.webp\" },\n { level: 15, file: \"bridge-15.webp\" },\n { level: 16, file: \"level-16.webp\" },\n { level: 17, file: \"level-17.webp\" },\n { level: 18, file: \"stage-18.webp\" },\n { level: 19, file: \"level-19.webp\" },\n { level: 21, file: \"level-21.webp\" },\n { level: 22, file: \"level-22.webp\" },\n { level: 24, file: \"stage-24.webp\" },\n { level: 25, file: \"level-25.webp\" },\n { level: 27, file: \"bridge-27.webp\" },\n { level: 28, file: \"level-28.webp\" },\n { level: 29, file: \"level-29.webp\" },\n { level: 30, file: \"stage-30.webp\" },\n] as const;\n\ninterface SkinSettings {\n enabled: boolean;\n bindEffort: boolean;\n}\n\ninterface PreferenceStore {\n getSnapshot(): SkinSettings;\n subscribe(listener: () => void): () => void;\n set(enabled: boolean): Promise;\n setBindEffort(enabled: boolean): Promise;\n dispose(): void;\n}\n\ntype NativeThemeId = \"light\" | \"dark\";\n\ninterface ThemeService {\n getTheme(): { preference: string };\n setTheme(id: NativeThemeId | \"system\"): void;\n}\n\ninterface ModelSelection {\n provider: string;\n model: string;\n reasoningEffort?: string;\n}\n\ninterface CatalogModel {\n id: string;\n reasoning?: {\n efforts: EffortLike[];\n defaultEffort?: string;\n };\n}\n\ninterface ModelDirectoryState {\n current: ModelSelection | null;\n groups: readonly { id: string; models: CatalogModel[] }[];\n status: \"idle\" | \"loading\" | \"ready\" | \"selecting\" | \"error\";\n error: string | null;\n}\n\ninterface ModelDirectory {\n store: SnapshotStore;\n load(): Promise;\n select(selection: ModelSelection): Promise<{ ok: boolean }>;\n}\n\ninterface SliderProps {\n directory: SnapshotStore;\n load: () => void;\n select: (selection: ModelSelection) => Promise;\n presenter: SkinPresenter;\n scope: PreferenceStore;\n}\n\ninterface SettingsRowProps {\n scope: PreferenceStore;\n presenter: SkinPresenter;\n theme: ThemeService;\n subscribeTheme: (listener: () => void) => () => void;\n t: (key: string) => string;\n}\n\nconst cssVariables = [\n \"--liang-strength\",\n \"--liang-page\",\n \"--liang-bg-base\",\n \"--liang-layer-1\",\n \"--liang-layer-2\",\n \"--liang-layer-3\",\n \"--liang-sidebar\",\n \"--liang-ink\",\n \"--liang-secondary\",\n \"--liang-tertiary\",\n \"--liang-border\",\n \"--liang-accent\",\n \"--liang-accent-hover\",\n \"--liang-hover\",\n \"--liang-portrait-opacity\",\n] as const;\n\nclass SkinPresenter {\n private readonly scope: PreferenceStore;\n private readonly theme: ThemeService;\n private readonly root: HTMLDivElement;\n private readonly portraits: ReturnType;\n private enabled = true;\n // Default to the max frame so the first paint after load is the dark shell;\n // starting at 0 flashed the light palette before the directory resolved.\n private frame = PREVIEW_MAX_FRAME;\n private pendingFrame = PREVIEW_MAX_FRAME;\n private raf = 0;\n private disposed = false;\n private unsubscribe: () => void;\n\n constructor(scope: PreferenceStore, theme: ThemeService) {\n this.scope = scope;\n this.theme = theme;\n this.root = document.createElement(\"div\");\n this.root.className = \"liang-skin-backdrop\";\n this.root.dataset.plugin = PACKAGE_ID;\n this.root.dataset.media = \"color\";\n this.root.setAttribute(\"aria-hidden\", \"true\");\n this.portraits = createPortraitLoader({\n sources: PORTRAIT_ANCHORS.map(({ level, file }) => ({\n level,\n url: `${ASSET_PREFIX}/portrait-source-v2/${file}`,\n })),\n onChange: ({ image }) => {\n if (this.disposed || !this.enabled) return;\n if (image === null) {\n this.root.dataset.media = \"color\";\n this.root.replaceChildren();\n return;\n }\n // Reuse the decoded node so a frame swap cannot start a second fetch.\n const portrait = image as HTMLImageElement;\n portrait.className = \"liang-skin-sequence-frame\";\n portrait.alt = \"\";\n portrait.draggable = false;\n if (this.root.firstChild !== portrait) this.root.replaceChildren(portrait);\n this.root.dataset.media = \"sequence\";\n },\n });\n\n this.unsubscribe = scope.subscribe(() => this.syncSettings());\n this.syncSettings();\n }\n\n private syncSettings() {\n this.setEnabled(this.scope.getSnapshot().enabled);\n }\n\n isEnabled() {\n return this.enabled;\n }\n\n getFrame() {\n return this.frame;\n }\n\n setEnabled(enabled: boolean) {\n if (this.disposed) return;\n this.enabled = enabled;\n if (enabled) {\n if (!this.root.isConnected) document.body.prepend(this.root);\n document.body.dataset.liangSkin = \"on\";\n this.applyFrame();\n this.portraits.start();\n } else {\n if (this.raf !== 0) cancelAnimationFrame(this.raf);\n this.raf = 0;\n this.root.remove();\n this.portraits.stop();\n this.root.replaceChildren();\n this.root.dataset.media = \"color\";\n delete document.body.dataset.liangSkin;\n delete document.body.dataset.liangStage;\n for (const name of cssVariables) document.body.style.removeProperty(name);\n }\n }\n\n setFrame(frame: number) {\n if (this.disposed) return;\n this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame)));\n this.frame = this.pendingFrame;\n if (!this.enabled) return;\n if (this.raf !== 0) return;\n this.raf = requestAnimationFrame(() => {\n this.raf = 0;\n this.frame = this.pendingFrame;\n this.applyFrame();\n });\n }\n\n private applyFrame() {\n if (this.disposed || !this.enabled) return;\n const palette = paletteForFrame(this.frame);\n const body = document.body;\n this.syncNativeTheme(palette.stage === 5 ? \"dark\" : \"light\");\n body.dataset.liangStage = String(palette.stage);\n body.style.setProperty(\"--liang-strength\", String(palette.strength));\n body.style.setProperty(\"--liang-page\", palette.page);\n body.style.setProperty(\"--liang-bg-base\", palette.base);\n body.style.setProperty(\"--liang-layer-1\", palette.layer1);\n body.style.setProperty(\"--liang-layer-2\", palette.layer2);\n body.style.setProperty(\"--liang-layer-3\", palette.layer3);\n body.style.setProperty(\"--liang-sidebar\", palette.sidebar);\n body.style.setProperty(\"--liang-ink\", palette.ink);\n body.style.setProperty(\"--liang-secondary\", palette.secondary);\n body.style.setProperty(\"--liang-tertiary\", palette.tertiary);\n body.style.setProperty(\"--liang-border\", palette.border);\n body.style.setProperty(\"--liang-accent\", palette.accent);\n body.style.setProperty(\"--liang-accent-hover\", palette.accentHover);\n body.style.setProperty(\"--liang-hover\", palette.hover);\n body.style.setProperty(\"--liang-portrait-opacity\", palette.portraitOpacity);\n this.updatePortrait(palette.level);\n }\n\n syncNativeTheme(theme: NativeThemeId = paletteForFrame(this.frame).stage === 5 ? \"dark\" : \"light\") {\n if (!this.enabled || this.theme.getTheme().preference === theme) return;\n this.theme.setTheme(theme);\n }\n\n private updatePortrait(level: number) {\n this.portraits.select(level);\n }\n\n async choose(enabled: boolean) {\n this.setEnabled(enabled);\n try {\n await this.scope.set(enabled);\n } catch (error) {\n this.syncSettings();\n throw error;\n }\n }\n\n dispose() {\n if (this.disposed) return;\n this.disposed = true;\n this.unsubscribe();\n if (this.raf !== 0) cancelAnimationFrame(this.raf);\n this.portraits.dispose();\n this.root.remove();\n document.body.removeAttribute(\"data-liang-skin\");\n delete document.body.dataset.liangStage;\n for (const name of cssVariables) document.body.style.removeProperty(name);\n }\n}\n\nfunction modelReasoning(state: ModelDirectoryState) {\n const current = state.current;\n if (current === null) return null;\n const group = state.groups.find((item) => item.id === current.provider);\n const model = group?.models.find((item) => item.id === current.model);\n if (model?.reasoning === undefined) return null;\n return {\n selection: current,\n efforts: model.reasoning.efforts,\n defaultEffort: model.reasoning.defaultEffort,\n };\n}\n\nfunction LiangEffortSlider({ directory, load, select, presenter, scope }: SliderProps) {\n const state = useSyncExternalStore(\n (listener) => directory.subscribe(listener),\n () => directory.getSnapshot(),\n );\n const skin = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n );\n const reasoning = useMemo(() => modelReasoning(state), [state]);\n const efforts = reasoning?.efforts ?? [];\n const committedIndex = selectedEffortIndex(\n efforts,\n reasoning?.selection.reasoningEffort,\n reasoning?.defaultEffort,\n );\n // An unknown committed effort defaults to the max frame: a fresh\n // conversation paints the dark shell immediately instead of flashing the\n // light palette until the directory load resolves the real effort.\n const committedFrame = committedIndex < 0\n ? PREVIEW_MAX_FRAME\n : frameForEffort(committedIndex, efforts.length);\n const bindEffort = skin.bindEffort;\n const [frame, setFrame] = useState(() => bindEffort ? committedFrame : presenter.getFrame());\n const [pending, setPending] = useState(false);\n const [failed, setFailed] = useState(false);\n const [interacting, setInteracting] = useState(false);\n const dragging = useRef(false);\n const dragStartFrame = useRef(frame);\n const enabled = skin.enabled;\n\n useEffect(() => {\n if (enabled) load();\n }, [enabled, load]);\n\n useEffect(() => {\n if (!bindEffort || dragging.current || pending) return;\n setFrame(committedFrame);\n presenter.setFrame(committedFrame);\n }, [bindEffort, committedFrame, pending, presenter]);\n\n if (!enabled) return null;\n if (bindEffort && (reasoning === null || efforts.length < 2)) return null;\n\n const previewIndex = nearestEffortIndex(frame, efforts);\n const previewEffort = efforts[previewIndex];\n const progressRatio = frame / PREVIEW_MAX_FRAME;\n const tooltipLabel = indicatorLabel(frame, bindEffort ? efforts : []);\n\n const commit = async (rawFrame: number) => {\n if (!bindEffort) {\n dragging.current = false;\n setFrame(rawFrame);\n presenter.setFrame(rawFrame);\n return;\n }\n const targetIndex = nearestEffortIndex(rawFrame, efforts);\n const target = efforts[targetIndex];\n if (target === undefined) return;\n const targetFrame = frameForEffort(targetIndex, efforts.length);\n dragging.current = false;\n setFrame(targetFrame);\n presenter.setFrame(targetFrame);\n if (targetIndex === committedIndex || pending) return;\n setPending(true);\n setFailed(false);\n const accepted = await select({\n provider: reasoning.selection.provider,\n model: reasoning.selection.model,\n reasoningEffort: target.id,\n });\n setPending(false);\n if (!accepted) {\n setFailed(true);\n setFrame(committedFrame);\n presenter.setFrame(committedFrame);\n }\n };\n\n return (\n \n {interacting && (\n \n {tooltipLabel}\n \n )}\n
\n {efforts.map((effort, index) => (\n \n ))}\n
\n {\n dragging.current = true;\n dragStartFrame.current = frame;\n setInteracting(true);\n }}\n onInput={(event) => {\n dragging.current = true;\n const next = Number(event.currentTarget.value);\n setFrame(next);\n presenter.setFrame(next);\n }}\n onPointerUp={(event) => {\n setInteracting(false);\n void commit(Number(event.currentTarget.value));\n }}\n onPointerCancel={() => {\n setInteracting(false);\n dragging.current = false;\n setFrame(dragStartFrame.current);\n presenter.setFrame(dragStartFrame.current);\n }}\n onKeyUp={(event) => {\n setInteracting(false);\n if (event.key !== \"Escape\") void commit(Number(event.currentTarget.value));\n }}\n onBlur={(event) => {\n setInteracting(false);\n if (dragging.current) void commit(Number(event.currentTarget.value));\n }}\n onKeyDown={(event) => {\n setInteracting(true);\n if (event.key === \"Escape\" && !pending) {\n setInteracting(false);\n dragging.current = false;\n setFrame(dragStartFrame.current);\n presenter.setFrame(dragStartFrame.current);\n }\n }}\n />\n \n );\n}\n\ntype AppearanceChoice = NativeThemeId | \"system\" | \"liang\";\n\nconst APPEARANCE_CHOICES: readonly { id: AppearanceChoice; labelKey: string }[] = [\n { id: \"light\", labelKey: \"appearance.light\" },\n { id: \"dark\", labelKey: \"appearance.dark\" },\n { id: \"system\", labelKey: \"appearance.system\" },\n { id: \"liang\", labelKey: \"appearance.liang\" },\n];\n\nfunction NativeAppearanceIcon({ id }: { id: Exclude }) {\n if (id === \"light\") {\n return (\n \n \n \n \n \n \n \n \n \n \n \n );\n }\n\n if (id === \"dark\") {\n return (\n \n \n \n );\n }\n\n return (\n \n \n \n \n );\n}\n\nfunction AppearanceSkinRow({ scope, presenter, theme, subscribeTheme, t }: SettingsRowProps) {\n const snapshot = useSyncExternalStore(\n (listener) => scope.subscribe(listener),\n () => scope.getSnapshot(),\n );\n const preference = useSyncExternalStore(\n subscribeTheme,\n () => theme.getTheme().preference,\n );\n const selected = snapshot.enabled\n ? \"liang\"\n : preference === \"light\" || preference === \"dark\" || preference === \"system\"\n ? preference\n : \"system\";\n const [pending, setPending] = useState(false);\n\n const choose = async (next: AppearanceChoice) => {\n if (next === selected || pending) return;\n setPending(true);\n try {\n if (next === \"liang\") {\n await presenter.choose(true);\n } else {\n // Disable the custom skin first. This is intentionally unconditional:\n // it makes the native choice the only writer after this click, even if\n // the external preference store is one render behind.\n await presenter.choose(false);\n theme.setTheme(next);\n }\n } finally {\n setPending(false);\n }\n };\n\n return (\n
\n {t(\"appearance.title\")}\n
\n {APPEARANCE_CHOICES.map(({ id, labelKey }) => (\n void choose(id)}\n key={id}\n >\n {id === \"liang\" ? (\n \u25C8\n ) : (\n \n )}\n {t(labelKey)}\n \n ))}\n
\n
\n );\n}\n\nexport const inject = [\n \"slots\",\n \"sessions\",\n \"modelDirectories\",\n \"locale\",\n \"theme\",\n];\n\nfunction createPreferenceStore(): PreferenceStore {\n // The market's active skin is the source of truth for whether this client\n // should be visible. The appearance switch is therefore scoped to this\n // client activation and must not survive switching away and back.\n try {\n localStorage.removeItem(\"dsh-liang-intensity-skin.enabled\");\n } catch {\n // Storage may be unavailable; the in-memory default still enables Liang.\n }\n let snapshot: SkinSettings = {\n enabled: true,\n bindEffort: localStorage.getItem(BIND_EFFORT_KEY) !== \"0\",\n };\n const listeners = new Set<() => void>();\n const onStorage = (event: StorageEvent) => {\n if (event.key !== BIND_EFFORT_KEY) return;\n const next = {\n enabled: snapshot.enabled,\n bindEffort: event.newValue !== \"0\",\n };\n if (next.enabled === snapshot.enabled && next.bindEffort === snapshot.bindEffort) return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n window.addEventListener(\"storage\", onStorage);\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async set(enabled) {\n if (enabled === snapshot.enabled) return;\n snapshot = { ...snapshot, enabled };\n for (const listener of listeners) listener();\n },\n async setBindEffort(bindEffort) {\n if (bindEffort === snapshot.bindEffort) return;\n localStorage.setItem(BIND_EFFORT_KEY, bindEffort ? \"1\" : \"0\");\n snapshot = { ...snapshot, bindEffort };\n for (const listener of listeners) listener();\n },\n dispose() {\n window.removeEventListener(\"storage\", onStorage);\n listeners.clear();\n },\n };\n}\n\nexport function apply(ctx: ClientContext) {\n const style = document.createElement(\"style\");\n style.dataset.plugin = PACKAGE_ID;\n style.textContent = styles;\n document.head.append(style);\n ctx.effect(() => () => style.remove(), \"liang-intensity-skin: scoped styles\");\n\n const scope = createPreferenceStore();\n ctx.effect(() => () => scope.dispose(), \"liang-intensity-skin: appearance preference\");\n const theme = ctx.get(\"theme\") as ThemeService;\n const presenter = new SkinPresenter(scope, theme);\n ctx.effect(() => () => presenter.dispose(), \"liang-intensity-skin: backdrop presenter\");\n ctx.effect(\n () => installLiangAppearanceButton(scope, presenter, PACKAGE_ID),\n \"liang-intensity-skin: native appearance extension\",\n );\n ctx.effect(\n () => ctx.locale.register(LOCALE_NAMESPACE, {\n zh: {\n \"appearance.title\": \"\u5916\u89C2\",\n \"appearance.light\": \"\u6D45\u8272\",\n \"appearance.dark\": \"\u6DF1\u8272\",\n \"appearance.system\": \"\u8DDF\u968F\u7CFB\u7EDF\",\n \"appearance.liang\": \"\u6ED1\u52A8\u53D8\u7956\",\n },\n en: {\n \"appearance.title\": \"Appearance\",\n \"appearance.light\": \"Light\",\n \"appearance.dark\": \"Dark\",\n \"appearance.system\": \"System\",\n \"appearance.liang\": \"Slider\",\n },\n }), \"liang-intensity-skin: settings locale\");\n\n ctx.slots.inject(\"conversation.input.right\", () => ctx.slots.register({\n name: \"conversation.input.right\",\n id: \"liang-intensity-control\",\n order: 10,\n inject: (sessionId: string) => {\n const available = ctx.sessions.subagentAddress(sessionId) === undefined;\n const directory = ctx.modelDirectories.directoryFor(sessionId) as ModelDirectory;\n return {\n directory: directory.store,\n presenter,\n scope,\n load: () => {\n if (available) void directory.load().catch(() => undefined);\n },\n select: (selection: ModelSelection) => available\n ? directory.select(selection).then((result) => result.ok, () => false)\n : Promise.resolve(false),\n };\n },\n }, LiangEffortSlider));\n}\n", ".liang-skin-backdrop {\n position: fixed;\n z-index: -1;\n inset: 0;\n overflow: hidden;\n pointer-events: none;\n background: var(--liang-page, transparent);\n opacity: 0;\n transition: opacity 220ms ease, background-color 180ms linear;\n}\n\nbody[data-liang-skin=\"on\"] .liang-skin-backdrop {\n opacity: 1;\n}\n\n.liang-skin-backdrop img {\n position: absolute;\n top: 50%;\n right: 0;\n display: block;\n width: min(42vw, 720px);\n height: 100%;\n object-fit: cover;\n object-position: 50% 42%;\n opacity: var(--liang-portrait-opacity, 0.45);\n transform: translateY(-50%);\n filter: none;\n mask-image: linear-gradient(90deg, transparent 0%, rgb(0 0 0 / 20%) 10%, black 30%, black 100%);\n transition: filter 180ms linear;\n}\n\n.liang-skin-backdrop img {\n display: none;\n}\n\n.liang-skin-backdrop[data-media=\"sequence\"] .liang-skin-sequence-frame {\n display: block;\n opacity: var(--liang-portrait-opacity, 0.82);\n}\n\n.liang-skin-backdrop[data-media=\"color\"] img {\n display: none;\n}\n\n.liang-skin-backdrop::after {\n position: absolute;\n inset: 0;\n content: \"\";\n background:\n linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%),\n linear-gradient(var(--liang-bg-base, transparent), var(--liang-bg-base, transparent));\n}\n\nbody[data-liang-skin=\"on\"] {\n /* Keep the backdrop above the canvas but below content and native portals.\n The host owns root positioning, dimensions, and its own stacking order. */\n isolation: isolate;\n --dsw-alias-bg-base: transparent !important;\n --dsw-alias-bg-layer-1: var(--liang-layer-1) !important;\n --dsw-alias-bg-layer-2: var(--liang-layer-2) !important;\n --dsw-alias-bg-layer-3: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-fill: var(--liang-sidebar) !important;\n --dsw-alias-label-primary: var(--liang-ink) !important;\n --dsw-alias-label-primary-dimmed: var(--liang-ink) !important;\n --dsw-alias-label-secondary: var(--liang-secondary) !important;\n --dsw-alias-label-tertiary: var(--liang-tertiary) !important;\n --dsw-alias-label-caption: var(--liang-tertiary) !important;\n --dsw-alias-border-l1: var(--liang-border) !important;\n --dsw-alias-border-l2-darkmode-thin: var(--liang-border) !important;\n --dsw-alias-border-l2: var(--liang-border) !important;\n --dsw-alias-border-l3: var(--liang-border) !important;\n --dsw-alias-button-primary-fill: var(--liang-accent) !important;\n --dsw-alias-button-primary-hover: var(--liang-accent-hover) !important;\n --dsw-alias-button-info-fill: var(--liang-accent) !important;\n --dsw-alias-button-info-hover: var(--liang-accent-hover) !important;\n --dsw-alias-state-business-primary: var(--liang-accent) !important;\n --dsw-alias-interactive-bg-hover: var(--liang-hover) !important;\n --dsw-alias-interactive-bg-active: var(--liang-hover) !important;\n --dsw-alias-button-elevated-fill: var(--liang-layer-1) !important;\n --dsw-alias-button-floating-fill: var(--liang-layer-2) !important;\n --dsw-alias-button-floating-hover: var(--liang-layer-3) !important;\n --dsw-alias-interactive-bg-hover-solid: var(--liang-layer-2) !important;\n --dsw-specific-bubble: var(--liang-layer-2) !important;\n --dsw-specific-input-major: var(--liang-layer-1) !important;\n --dsw-specific-menu: var(--liang-layer-3) !important;\n --dsw-specific-selector: var(--liang-layer-2) !important;\n --dsw-alias-markdown-citation: var(--liang-layer-2) !important;\n --dsw-alias-markdown-code-block-banner: var(--liang-layer-1) !important;\n --dsw-alias-markdown-code-block: var(--liang-layer-1) !important;\n --dsw-alias-markdown-code-segment-selected: var(--liang-layer-2) !important;\n --dsw-alias-markdown-code-segment-unselected: var(--liang-layer-1) !important;\n --dsw-alias-markdown-inline-code: var(--liang-layer-2) !important;\n --dsw-alias-markdown-placeholder: var(--liang-layer-1) !important;\n --dsw-alias-markdown-tag: var(--liang-layer-2) !important;\n background: var(--liang-page) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) {\n --dsw-alias-brand-primary-new-colorprimary-new-color: rgb(65 118 230) !important;\n --dsw-alias-brand-primary: rgb(15 17 21) !important;\n --dsw-alias-brand-text: rgb(15 17 21) !important;\n --dsw-alias-button-primary-fill: rgb(15 17 21) !important;\n --dsw-alias-button-primary-hover: rgb(53 54 56) !important;\n --dsw-alias-button-info-fill: rgb(65 118 230) !important;\n --dsw-alias-button-info-hover: rgb(103 158 254) !important;\n --dsw-alias-state-business-primary: rgb(65 118 230) !important;\n --dsw-alias-label-primary-inverted: rgb(249 250 251) !important;\n --dsw-alias-button-elevated-fill: rgb(255 255 255) !important;\n --dsw-alias-button-floating-fill: rgb(255 255 255) !important;\n --dsw-alias-button-floating-hover: rgb(241 243 245) !important;\n --dsw-alias-interactive-bg-hover-solid: rgb(241 243 245) !important;\n --dsw-specific-bubble: rgb(237 243 254) !important;\n --dsw-specific-input-major: rgb(255 255 255) !important;\n --dsw-specific-menu: rgb(255 255 255) !important;\n --dsw-specific-selector: rgb(245 246 247) !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] {\n --dsw-alias-brand-primary-new-colorprimary-new-color: #c19a49 !important;\n --dsw-alias-brand-primary: #c19a49 !important;\n --dsw-alias-brand-text: #d5b56e !important;\n --dsw-alias-button-info-fill: #c19a49 !important;\n --dsw-alias-button-info-hover: #d5b56e !important;\n --dsw-alias-state-business-primary: #c19a49 !important;\n --dsw-alias-bg-module-platform: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-nav-item-active: var(--liang-layer-3) !important;\n --dsw-specific-sidebar-nav-item-hover: var(--liang-hover) !important;\n}\n\n/* The host renders the running-turn label as a clipped blue text gradient.\n Retarget only that gradient's color tokens in the custom dark skin; keep\n the host's text clipping and shimmer behavior, and leave light mode alone. */\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"_turnStatus\"] {\n --dsw-static-deepseek-500: #c19a49 !important;\n --dsw-static-deepseek-200: #d5b56e !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"_turnStatusClock\"] {\n color: #c19a49 !important;\n -webkit-text-fill-color: #c19a49 !important;\n}\n\n/* Preserve the wordmark badge contrast in the low-intensity shell. */\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > rect[width=\"52\"] {\n fill: rgb(15 17 21) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > g[clip-path*=\"badge\"] path {\n fill: rgb(249 250 251) !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > rect[width=\"52\"] {\n fill: #c19a49 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u65B0\u5EFA\u4F1A\u8BDD\"] svg[viewBox=\"0 0 182 24\"] > g[clip-path*=\"badge\"] path {\n fill: #171816 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [aria-label^=\"\u9009\u62E9\u6A21\u578B\"] > span:nth-of-type(2) {\n color: #d5b56e !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] [class*=\"heroGlow\"] ellipse {\n fill: #c19a49 !important;\n fill-opacity: 0.16 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] span[class*=\"_previewBadge\"] {\n color: #171816 !important;\n background: #c19a49 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u53D1\u9001\u6D88\u606F\"] {\n color: #171816 !important;\n}\n\nbody[data-liang-skin=\"on\"][data-liang-stage=\"5\"] button[aria-label=\"\u53D1\u9001\u6D88\u606F\"]:disabled {\n opacity: 0.62;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u53D1\u9001\u6D88\u606F\"] {\n color: #fff !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) button[aria-label=\"\u53D1\u9001\u6D88\u606F\"]:disabled {\n color: rgb(255 255 255 / 92%) !important;\n background: rgb(65 118 230 / 42%) !important;\n opacity: 1;\n}\n\nbody[data-liang-skin=\"on\"] > #root {\n background: transparent !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-dsh-desktop-mode=\"compatibility\"],\n [data-dsh-desktop-mode=\"extended\"]\n) .liang-skin-backdrop {\n top: var(--dsh-desktop-frame-height, 0px);\n}\n\n/* Windows paints the sidebar fill across the host frame, covering the portrait.\n Match macOS's transparent frame while keeping the sidebar, caption, and\n conversation surfaces painted by their own host rules. */\nhtml[data-windows-titlebar] body[data-liang-skin=\"on\"] #root [class*=\"_frame\"]:has(> [class*=\"_centerCol\"]) {\n background: transparent !important;\n}\n\n/* macOS adds different translucent sidebar materials for native light/dark.\n While Liang is active, the effort palette owns the sidebar fill. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_frame\"]:has(> [class*=\"_centerCol\"]) > [class*=\"_sidebarCol\"] {\n background: var(--liang-sidebar) !important;\n}\n\n/* Keep the host's expanded new-session surface on the same palette. The\n collapsed icon keeps its native transparent surface and hover behavior. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"]:has(> [class*=\"_logoRow\"]):not([class*=\"_collapsed\"]) button[class*=\"_newSession\"] {\n background: var(--dsw-alias-button-elevated-fill) !important;\n}\n\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"]:has(> [class*=\"_logoRow\"]):not([class*=\"_collapsed\"]) button[class*=\"_newSession\"]:hover {\n background: var(--dsw-alias-button-floating-hover) !important;\n}\n\n/* The native dark rule also outranks the collapsed icon's transparent fill. */\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"][class*=\"_collapsed\"]:has(> [class*=\"_logoRow\"]) button[class*=\"_newSession\"] {\n background: transparent !important;\n}\n\nhtml[data-platform=\"darwin\"] body[data-liang-skin=\"on\"] #root [class*=\"_sidebarCol\"] [class*=\"_root\"][class*=\"_collapsed\"]:has(> [class*=\"_logoRow\"]) button[class*=\"_newSession\"]:hover {\n background: var(--dsw-alias-interactive-bg-hover) !important;\n}\n\nbody[data-liang-skin=\"on\"] button,\nbody[data-liang-skin=\"on\"] input,\nbody[data-liang-skin=\"on\"] textarea {\n transition: color 160ms linear, background-color 160ms linear, border-color 160ms linear;\n}\n\n/* The composer card has a native theme-mode fill, so keep its surface and\n readable ink aligned with the active Liang palette. */\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] {\n border-color: var(--liang-border) !important;\n color: var(--liang-ink) !important;\n background: var(--liang-layer-1) !important;\n box-shadow: 0 8px 28px rgb(20 22 20 / 8%);\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] :is(textarea, button, [data-input-mirror=\"true\"]) {\n color: var(--liang-ink) !important;\n caret-color: var(--liang-accent) !important;\n}\n\nbody[data-liang-skin=\"on\"]:is(\n [data-liang-stage=\"0\"],\n [data-liang-stage=\"1\"],\n [data-liang-stage=\"2\"],\n [data-liang-stage=\"3\"]\n) [data-composer-card=\"true\"] textarea::placeholder {\n color: var(--liang-tertiary) !important;\n opacity: 1;\n}\n\n.liang-effort-control {\n --liang-control-accent: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6);\n --liang-control-rail: var(--dsw-alias-border-l3, rgb(0 0 0 / 16%));\n position: relative;\n display: flex;\n width: 124px;\n height: 28px;\n align-items: center;\n margin: 0 3px;\n overflow: visible;\n}\n\nbody[data-liang-skin=\"on\"] .liang-effort-control {\n --liang-control-accent: var(--liang-secondary);\n --liang-control-rail: color-mix(in srgb, var(--liang-secondary) 28%, transparent);\n}\n\n.liang-effort-control__ticks {\n position: absolute;\n z-index: 0;\n inset: 0 10px;\n pointer-events: none;\n}\n\n.liang-effort-control__ticks::before {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: 1px;\n content: \"\";\n background: var(--liang-control-rail);\n transform: translateY(-50%);\n}\n\n.liang-effort-control__tooltip {\n position: absolute;\n z-index: 4;\n bottom: calc(100% + 7px);\n left: calc(10px + (100% - 20px) * var(--liang-slider-ratio));\n min-width: max-content;\n padding: 5px 8px;\n border: 1px solid var(--liang-border, rgb(0 0 0 / 12%));\n border-radius: 7px;\n color: var(--liang-ink, #171816);\n background: var(--liang-layer-1, #fff);\n box-shadow: 0 5px 16px rgb(0 0 0 / 16%);\n font-size: 11px;\n font-weight: 600;\n line-height: 16px;\n letter-spacing: 0.01em;\n pointer-events: none;\n transform: translateX(-50%);\n white-space: nowrap;\n}\n\n.liang-effort-control__tooltip::after {\n position: absolute;\n top: 100%;\n left: 50%;\n width: 7px;\n height: 7px;\n content: \"\";\n background: inherit;\n transform: translate(-50%, -4px) rotate(45deg);\n}\n\n.liang-effort-control__tick {\n position: absolute;\n top: 50%;\n width: 1px;\n height: 7px;\n background: var(--liang-control-accent);\n opacity: 0.42;\n transform: translate(-50%, -50%);\n}\n\n.liang-effort-control__range {\n position: relative;\n z-index: 1;\n width: calc(100% - 7px);\n height: 28px;\n margin: 0 3.5px;\n cursor: ew-resize;\n appearance: none;\n background: transparent;\n touch-action: pan-y;\n}\n\n.liang-effort-control__range:disabled {\n cursor: progress;\n opacity: 0.58;\n}\n\n.liang-effort-control__range::-webkit-slider-runnable-track {\n height: 1px;\n border-radius: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-webkit-slider-thumb {\n width: 13px;\n height: 13px;\n margin-top: -6px;\n border: 1.5px solid color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n border-radius: 50%;\n appearance: none;\n background: radial-gradient(circle, var(--liang-control-accent) 0 2px, color-mix(in srgb, var(--liang-control-accent) 22%, var(--dsw-alias-bg-layer-1, #fff)) 2.5px);\n box-shadow: 0 1px 4px rgb(0 0 0 / 14%);\n}\n\nbody[data-liang-skin=\"on\"] .liang-effort-control__range::-webkit-slider-thumb {\n width: 13px;\n height: 13px;\n margin-top: -6px;\n border-color: color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n background: radial-gradient(circle, var(--liang-control-accent) 0 2px, color-mix(in srgb, var(--liang-control-accent) 22%, var(--liang-layer-1)) 2.5px);\n}\n\n.liang-effort-control__range::-moz-range-track {\n height: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-moz-range-progress {\n height: 1px;\n background: transparent;\n}\n\n.liang-effort-control__range::-moz-range-thumb {\n width: 11px;\n height: 11px;\n border: 1.5px solid color-mix(in srgb, var(--liang-control-accent) 72%, transparent);\n border-radius: 50%;\n background: color-mix(in srgb, var(--liang-control-accent) 24%, var(--dsw-alias-bg-layer-1, #fff));\n}\n\n.liang-effort-control__range:focus-visible {\n outline: none;\n}\n\n.liang-effort-control[data-state=\"error\"] {\n --liang-control-accent: var(--dsw-alias-state-error-primary, #e43c3c);\n}\n\n.liang-appearance-choice {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n flex: 180px;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: 4px;\n padding: 20px 32px;\n border-radius: 16px;\n font: inherit;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: transparent;\n font-size: 14px;\n line-height: 22px;\n display: flex;\n}\n\n[class*=\"_cubeRow\"]:has(.liang-appearance-choice) {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n}\n\n.liang-appearance-choice:hover:not([aria-pressed=\"true\"]) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.liang-appearance-choice[aria-pressed=\"true\"] {\n border-color: var(--dsw-static-neutral-bluish-400);\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.liang-appearance-choice__icon {\n display: block;\n flex: 0 0 16px;\n width: 16px;\n height: 16px;\n color: currentColor;\n font-size: 16px;\n line-height: 16px;\n}\n\n.liang-appearance-choice__label {\n display: block;\n}\n\n/* Keep the host's three theme cubes untouched. These rules only style the\n fourth button that is appended to the host cube row. */\n.liang-appearance-choice:disabled {\n cursor: progress;\n opacity: 0.7;\n}\n\n.liang-appearance-binding {\n box-sizing: border-box;\n display: flex;\n width: 100%;\n min-height: 28px;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 4px 0 0;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 14px;\n line-height: 22px;\n cursor: pointer;\n}\n\n.liang-appearance-binding__label {\n display: block;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.liang-appearance-binding__copy {\n display: flex;\n min-width: 0;\n flex-direction: column;\n gap: 1px;\n}\n\n.liang-appearance-binding__description {\n display: block;\n overflow: hidden;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.liang-appearance-binding__input {\n position: relative;\n flex: 0 0 36px;\n width: 36px;\n height: 20px;\n margin: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n outline: none;\n cursor: pointer;\n appearance: none;\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-module-platform));\n}\n\n.liang-appearance-binding__input::before {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n content: \"\";\n background: var(--dsw-alias-label-primary);\n box-shadow: 0 1px 3px rgb(0 0 0 / 18%);\n transition: transform 160ms ease, background-color 160ms ease;\n}\n\n.liang-appearance-binding__input:checked {\n border-color: var(--dsw-static-neutral-bluish-400);\n background: var(--dsw-static-neutral-bluish-400);\n}\n\n.liang-appearance-binding__input:checked::before {\n background: var(--dsw-alias-bg-layer-1, #fff);\n transform: translateX(16px);\n}\n\n.liang-appearance-binding__input:focus-visible {\n box-shadow: 0 0 0 2px var(--dsw-alias-interactive-bg-hover);\n}\n\n.liang-appearance-binding__input:disabled {\n cursor: progress;\n opacity: 0.7;\n}\n\n@media (max-width: 760px) {\n .liang-effort-control {\n width: 92px;\n }\n\n .liang-skin-backdrop img {\n right: -18vw;\n width: 92vw;\n opacity: calc(var(--liang-portrait-opacity, 0.45) * 0.72);\n }\n\n}\n\n@media (prefers-reduced-motion: reduce) {\n .liang-skin-backdrop,\n .liang-skin-backdrop img,\n body[data-liang-skin=\"on\"] button,\n body[data-liang-skin=\"on\"] input,\n body[data-liang-skin=\"on\"] textarea {\n transition: none;\n }\n}\n", "export interface AppearanceSnapshot {\n enabled: boolean;\n bindEffort: boolean;\n}\n\nexport interface AppearanceScope {\n getSnapshot(): AppearanceSnapshot;\n subscribe(listener: () => void): () => void;\n setBindEffort(enabled: boolean): Promise;\n}\n\nexport interface AppearancePresenter {\n choose(enabled: boolean): Promise;\n}\n\nconst NATIVE_APPEARANCE_ROW = '[class*=\"_cubeRow\"]';\nconst LIANG_APPEARANCE_BUTTON = \"liang-appearance-choice\";\nconst LIANG_BINDING_CONTROL = \"liang-appearance-binding\";\nconst LIANG_BINDING_INPUT = \"liang-appearance-binding__input\";\n\nfunction hasClassSuffix(node: Element, suffix: string) {\n return [...node.classList].some((name) => name.endsWith(suffix));\n}\n\nfunction findAppearanceRow() {\n for (const row of document.querySelectorAll(NATIVE_APPEARANCE_ROW)) {\n if (!hasClassSuffix(row, \"_cubeRow\")) continue;\n // All three native appearance choices are direct button children. Validate\n // the structure as well as the suffix so unrelated cube rows stay alone.\n const nativeButtons = [...row.children].filter(\n (node): node is HTMLButtonElement => node instanceof HTMLButtonElement\n && hasClassSuffix(node, \"_themeCube\"),\n );\n if (nativeButtons.length < 3) continue;\n\n let group = row.parentElement;\n while (group !== null && !hasClassSuffix(group, \"_group\")) {\n group = group.parentElement;\n }\n group ??= row.parentElement;\n if (group !== null) return { row, group, nativeButtons };\n }\n return null;\n}\n\ninterface MountedAppearance {\n row: HTMLElement;\n group: HTMLElement;\n button: HTMLButtonElement;\n binding: HTMLLabelElement;\n bindingInput: HTMLInputElement;\n handleCustomClick: () => void;\n handleBindingChange: () => void;\n}\n\nexport function installLiangAppearanceButton(\n scope: AppearanceScope,\n presenter: AppearancePresenter,\n packageId: string,\n): () => void {\n let pending = false;\n let disposed = false;\n let mounted: MountedAppearance | null = null;\n const nativeClickHandlers = new Map void>();\n\n const unmount = () => {\n for (const [button, handler] of nativeClickHandlers) {\n button.removeEventListener(\"click\", handler, { capture: true });\n }\n nativeClickHandlers.clear();\n if (mounted === null) return;\n mounted.button.removeEventListener(\"click\", mounted.handleCustomClick);\n mounted.bindingInput.removeEventListener(\"change\", mounted.handleBindingChange);\n mounted.button.remove();\n mounted.binding.remove();\n mounted = null;\n };\n\n const choose = async (enabled: boolean) => {\n if (disposed || pending || scope.getSnapshot().enabled === enabled) return;\n pending = true;\n sync();\n try {\n await presenter.choose(enabled);\n } catch {\n // The preference store remains the source of truth after a failed choice.\n } finally {\n pending = false;\n if (!disposed) sync();\n }\n };\n\n const createMount = (row: HTMLElement, group: HTMLElement): MountedAppearance => {\n const english = document.documentElement.lang.toLowerCase().startsWith(\"en\");\n const button = document.createElement(\"button\");\n button.className = LIANG_APPEARANCE_BUTTON;\n button.type = \"button\";\n button.dataset.plugin = packageId;\n button.setAttribute(\"aria-label\", \"\u6ED1\u52A8\u53D8\u7956\");\n\n const icon = document.createElement(\"span\");\n icon.className = \"liang-appearance-choice__icon\";\n icon.setAttribute(\"aria-hidden\", \"true\");\n icon.textContent = \"\u25C8\";\n\n const label = document.createElement(\"span\");\n label.className = \"liang-appearance-choice__label\";\n label.textContent = english ? \"Slider\" : \"\u6ED1\u52A8\u53D8\u7956\";\n button.append(icon, label);\n const handleCustomClick = () => { void choose(true); };\n button.addEventListener(\"click\", handleCustomClick);\n\n const binding = document.createElement(\"label\");\n binding.className = LIANG_BINDING_CONTROL;\n binding.dataset.plugin = packageId;\n\n const bindingCopy = document.createElement(\"span\");\n bindingCopy.className = \"liang-appearance-binding__copy\";\n const bindingLabel = document.createElement(\"span\");\n bindingLabel.className = \"liang-appearance-binding__label\";\n const bindingText = english ? \"Bind slider to reasoning level\" : \"\u6ED1\u52A8\u53D8\u7956\u7ED1\u5B9A\u601D\u8003\u7B49\u7EA7\";\n bindingLabel.textContent = bindingText;\n\n const bindingDescription = document.createElement(\"span\");\n bindingDescription.className = \"liang-appearance-binding__description\";\n bindingDescription.id = \"liang-appearance-binding-description\";\n bindingDescription.textContent = english\n ? \"When off, the slider does not change the reasoning level.\"\n : \"\u5173\u95ED\u4E4B\u540E\u6ED1\u5757\u4E0D\u8054\u52A8\u601D\u8003\u7B49\u7EA7\";\n bindingCopy.append(bindingLabel, bindingDescription);\n\n const bindingInput = document.createElement(\"input\");\n bindingInput.className = LIANG_BINDING_INPUT;\n bindingInput.type = \"checkbox\";\n bindingInput.setAttribute(\"role\", \"switch\");\n bindingInput.setAttribute(\"aria-label\", bindingText);\n bindingInput.setAttribute(\"aria-describedby\", bindingDescription.id);\n const updateBinding = async () => {\n if (disposed || pending) return;\n try {\n await scope.setBindEffort(bindingInput.checked);\n } catch {\n // Restore the persisted selection if writing the preference fails.\n } finally {\n if (!disposed) sync();\n }\n };\n const handleBindingChange = () => { void updateBinding(); };\n bindingInput.addEventListener(\"change\", handleBindingChange);\n binding.append(bindingCopy, bindingInput);\n\n return { row, group, button, binding, bindingInput, handleCustomClick, handleBindingChange };\n };\n\n const sync = () => {\n if (disposed) return;\n const target = findAppearanceRow();\n if (target === null) {\n unmount();\n return;\n }\n if (mounted !== null && (mounted.row !== target.row || mounted.group !== target.group)) {\n unmount();\n }\n mounted ??= createMount(target.row, target.group);\n const { row, group, button, binding, bindingInput } = mounted;\n if (button.parentElement !== row) row.append(button);\n\n const snapshot = scope.getSnapshot();\n button.disabled = pending;\n button.setAttribute(\"aria-pressed\", String(snapshot.enabled));\n if (snapshot.enabled) {\n if (binding.parentElement !== group) group.append(binding);\n bindingInput.checked = snapshot.bindEffort;\n bindingInput.setAttribute(\"aria-checked\", String(snapshot.bindEffort));\n bindingInput.disabled = pending;\n } else {\n binding.remove();\n }\n\n for (const [nativeButton, handler] of nativeClickHandlers) {\n if (target.nativeButtons.includes(nativeButton)) continue;\n nativeButton.removeEventListener(\"click\", handler, { capture: true });\n nativeClickHandlers.delete(nativeButton);\n }\n for (const nativeButton of target.nativeButtons) {\n if (nativeClickHandlers.has(nativeButton)) continue;\n const handleNativeClick = () => { void choose(false); };\n nativeClickHandlers.set(nativeButton, handleNativeClick);\n nativeButton.addEventListener(\"click\", handleNativeClick, { capture: true });\n }\n };\n\n const observer = new MutationObserver(sync);\n observer.observe(document.body, { childList: true, subtree: true });\n const unsubscribe = scope.subscribe(sync);\n sync();\n\n return () => {\n if (disposed) return;\n // Mark disposal first: outstanding choices and queued observer callbacks\n // must never attach new controls after the client has been deactivated.\n disposed = true;\n observer.disconnect();\n unsubscribe();\n unmount();\n };\n}\n", "export interface PortraitSource {\n level: number;\n url: string;\n}\n\nexport interface PortraitState {\n index: number | null;\n url: string | null;\n status: \"loading\" | \"ready\" | \"failed\";\n image: PortraitImage | null;\n}\n\nexport interface PortraitImage {\n src: string;\n loading: \"eager\" | \"lazy\";\n decoding: \"async\" | \"sync\" | \"auto\";\n decode(): Promise;\n addEventListener(type: \"load\" | \"error\", listener: () => void): void;\n removeEventListener(type: \"load\" | \"error\", listener: () => void): void;\n removeAttribute(name: string): void;\n}\n\nexport interface PortraitLoaderOptions {\n sources: readonly PortraitSource[];\n onChange: (state: PortraitState) => void;\n createImage?: () => PortraitImage;\n}\n\ninterface Asset {\n url: string;\n status: \"idle\" | \"loading\" | \"ready\" | \"failed\";\n loadedUrl: string | null;\n retries: number;\n image: PortraitImage | null;\n timer: ReturnType | null;\n detach: (() => void) | null;\n}\n\nconst RETRY_DELAYS = [500, 1500, 3000] as const;\n\nfunction retryUrl(source: string, retry: number) {\n if (retry === 0) return source;\n const hashIndex = source.indexOf(\"#\");\n const hash = hashIndex < 0 ? \"\" : source.slice(hashIndex);\n const base = hashIndex < 0 ? source : source.slice(0, hashIndex);\n const queryIndex = base.indexOf(\"?\");\n const path = queryIndex < 0 ? base : base.slice(0, queryIndex);\n const query = new URLSearchParams(queryIndex < 0 ? \"\" : base.slice(queryIndex + 1));\n query.set(\"retry\", String(retry));\n return `${path}?${query}${hash}`;\n}\n\n/**\n * Decode frames independently and return the decoded image itself for display.\n * start() resumes after stop(); stop() silently releases every request/image.\n * dispose() is permanent. select() also accepts a target before start().\n */\nexport function createPortraitLoader(options: PortraitLoaderOptions) {\n const sources = options.sources.map((source) => ({ ...source }));\n const assets = new Map();\n for (const { url } of sources) {\n if (!assets.has(url)) {\n assets.set(url, {\n url, status: \"idle\", loadedUrl: null, retries: 0,\n image: null, timer: null, detach: null,\n });\n }\n }\n const createImage = options.createImage ?? (() => new Image());\n let onChange: PortraitLoaderOptions[\"onChange\"] | null = options.onChange;\n let active = false;\n let disposed = false;\n let generation = 0;\n let targetLevel = sources[0]?.level ?? 0;\n let displayedIndex: number | null = null;\n let lastState: PortraitState | null = null;\n\n const isCurrent = (run: number) => active && !disposed && generation === run;\n const nearestIndex = (available: (index: number) => boolean) => {\n let nearest: number | null = null;\n for (let index = 0; index < sources.length; index += 1) {\n if (!available(index)) continue;\n const distance = Math.abs(sources[index].level - targetLevel);\n const previous = nearest === null ? Infinity : Math.abs(sources[nearest].level - targetLevel);\n if (nearest === null || distance < previous || (distance === previous && sources[index].level > sources[nearest].level)) {\n nearest = index;\n }\n }\n return nearest;\n };\n const isReady = (index: number) => assets.get(sources[index].url)?.status === \"ready\";\n const emit = (state: PortraitState) => {\n if (lastState?.index === state.index && lastState.url === state.url && lastState.status === state.status) return;\n lastState = state;\n onChange?.(state);\n };\n const reconcile = () => {\n if (!active || disposed) return;\n const target = nearestIndex(() => true);\n if (target !== null && isReady(target)) displayedIndex = target;\n else if (displayedIndex === null) displayedIndex = nearestIndex(isReady);\n if (displayedIndex !== null) {\n const asset = assets.get(sources[displayedIndex].url)!;\n emit({ index: displayedIndex, url: asset.loadedUrl, image: asset.image, status: \"ready\" });\n } else {\n const exhausted = [...assets.values()].every((asset) => asset.status === \"failed\");\n emit({ index: null, url: null, image: null, status: exhausted ? \"failed\" : \"loading\" });\n }\n };\n\n const request = (asset: Asset, run: number) => {\n if (!isCurrent(run)) return;\n asset.status = \"loading\";\n const image = createImage();\n asset.image = image;\n image.loading = \"eager\";\n image.decoding = \"async\";\n const url = retryUrl(asset.url, asset.retries);\n let settled = false;\n let decoding = false;\n const current = () => !settled && isCurrent(run) && asset.image === image;\n const detach = () => {\n image.removeEventListener(\"load\", handleLoad);\n image.removeEventListener(\"error\", handleError);\n asset.detach = null;\n };\n const handleError = () => {\n if (!current()) return;\n settled = true;\n detach();\n image.removeAttribute(\"src\");\n asset.image = null;\n if (asset.retries < RETRY_DELAYS.length) {\n const delay = RETRY_DELAYS[asset.retries];\n asset.retries += 1;\n asset.timer = setTimeout(() => {\n if (!isCurrent(run)) return;\n asset.timer = null;\n request(asset, run);\n }, delay);\n } else {\n asset.status = \"failed\";\n reconcile();\n }\n };\n const handleLoad = () => {\n if (!current() || decoding) return;\n decoding = true;\n void image.decode().then(() => {\n if (!current()) return;\n settled = true;\n detach();\n asset.loadedUrl = url;\n asset.status = \"ready\";\n reconcile();\n }, handleError);\n };\n asset.detach = detach;\n image.addEventListener(\"load\", handleLoad);\n image.addEventListener(\"error\", handleError);\n image.src = url;\n };\n\n const stop = () => {\n active = false;\n generation += 1;\n for (const asset of assets.values()) {\n if (asset.timer !== null) clearTimeout(asset.timer);\n asset.timer = null;\n asset.detach?.();\n asset.image?.removeAttribute(\"src\");\n asset.image = null;\n asset.status = \"idle\";\n asset.loadedUrl = null;\n asset.retries = 0;\n }\n displayedIndex = null;\n lastState = null;\n };\n\n return {\n start() {\n if (disposed || active) return;\n active = true;\n const run = ++generation;\n reconcile();\n for (const asset of assets.values()) request(asset, run);\n },\n select(level: number) {\n if (disposed || !Number.isFinite(level)) return;\n targetLevel = level;\n reconcile();\n },\n stop,\n dispose() {\n stop();\n disposed = true;\n onChange = null;\n },\n };\n}\n", "export const PREVIEW_MAX_FRAME = 240;\nexport const MAX_LEVEL = 30;\n\nconst LIANG_RANKS = [\"\u5C0F\u96BE\u6881\", \"\u7262\u6881\", \"\u6881\u5B50\", \"\u6881\u5723\", \"\u6881\u795E\", \"\u6881\u7956\"] as const;\n\nexport interface EffortLike {\n id: string;\n name: string;\n description?: string;\n}\n\nexport interface Palette {\n level: number;\n stage: number;\n strength: number;\n page: string;\n base: string;\n layer1: string;\n layer2: string;\n layer3: string;\n sidebar: string;\n ink: string;\n secondary: string;\n tertiary: string;\n border: string;\n accent: string;\n accentHover: string;\n hover: string;\n portraitOpacity: string;\n}\n\ntype Rgb = readonly [number, number, number];\n\ninterface PaletteStop {\n at: number;\n page: Rgb;\n surface: Rgb;\n surface2: Rgb;\n ink: Rgb;\n secondary: Rgb;\n accent: Rgb;\n portraitOpacity: number;\n}\n\n// These are the original calibrator's six stage anchors: pale neutral at the\n// low end, progressively smoked surfaces, then charcoal and antique gold.\nconst STOPS: readonly PaletteStop[] = [\n { at: 0, page: [232, 233, 229], surface: [248, 248, 245], surface2: [238, 239, 235], ink: [23, 24, 22], secondary: [112, 116, 111], accent: [181, 43, 36], portraitOpacity: 0.92 },\n { at: 6, page: [211, 211, 206], surface: [239, 239, 234], surface2: [224, 224, 218], ink: [26, 26, 24], secondary: [101, 101, 96], accent: [181, 43, 36], portraitOpacity: 0.93 },\n { at: 12, page: [171, 168, 162], surface: [211, 208, 201], surface2: [190, 186, 179], ink: [27, 26, 24], secondary: [83, 79, 74], accent: [166, 54, 42], portraitOpacity: 0.94 },\n { at: 18, page: [117, 112, 106], surface: [154, 148, 140], surface2: [128, 122, 115], ink: [24, 22, 20], secondary: [65, 61, 56], accent: [154, 56, 42], portraitOpacity: 0.94 },\n { at: 24, page: [43, 39, 37], surface: [48, 43, 40], surface2: [58, 51, 46], ink: [244, 241, 232], secondary: [184, 180, 169], accent: [181, 92, 54], portraitOpacity: 0.94 },\n { at: 30, page: [17, 17, 17], surface: [30, 27, 25], surface2: [41, 35, 30], ink: [244, 241, 232], secondary: [184, 180, 169], accent: [193, 154, 73], portraitOpacity: 0.94 },\n];\n\nexport function clampFrame(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(value)));\n}\n\nexport function frameForEffort(index: number, count: number): number {\n if (count <= 1 || !Number.isFinite(index)) return 0;\n const safe = Math.min(count - 1, Math.max(0, Math.round(index)));\n return Math.round((safe / (count - 1)) * PREVIEW_MAX_FRAME);\n}\n\nexport function nearestEffortIndex(frame: number, efforts: readonly EffortLike[]): number {\n if (efforts.length === 0) return -1;\n const safe = clampFrame(frame);\n let best = 0;\n let distance = Math.abs(safe - frameForEffort(0, efforts.length));\n for (let index = 1; index < efforts.length; index += 1) {\n const next = Math.abs(safe - frameForEffort(index, efforts.length));\n if (next < distance) {\n best = index;\n distance = next;\n }\n }\n return best;\n}\n\nexport function selectedEffortIndex(\n efforts: readonly EffortLike[],\n selectedId?: string,\n defaultId?: string,\n): number {\n const id = selectedId ?? defaultId;\n return id === undefined ? -1 : efforts.findIndex((effort) => effort.id === id);\n}\n\nexport function portraitBlendForLevel(level: number, anchors: readonly number[]) {\n if (anchors.length === 0) return { lowerIndex: -1, upperIndex: -1, mix: 0 };\n const safeLevel = Number.isFinite(level)\n ? Math.min(anchors[anchors.length - 1], Math.max(anchors[0], level))\n : anchors[0];\n let upperIndex = anchors.findIndex((anchor) => anchor >= safeLevel);\n if (upperIndex < 0) upperIndex = anchors.length - 1;\n const lowerIndex = Math.max(0, upperIndex - (anchors[upperIndex] > safeLevel ? 1 : 0));\n const span = anchors[upperIndex] - anchors[lowerIndex];\n return {\n lowerIndex,\n upperIndex,\n mix: span === 0 ? 0 : (safeLevel - anchors[lowerIndex]) / span,\n };\n}\n\nexport function liangRankForFrame(rawFrame: number): string {\n const level = (clampFrame(rawFrame) / PREVIEW_MAX_FRAME) * MAX_LEVEL;\n const index = level >= MAX_LEVEL ? LIANG_RANKS.length - 1 : Math.floor(level / 6);\n return LIANG_RANKS[Math.min(LIANG_RANKS.length - 1, Math.max(0, index))];\n}\n\nexport function indicatorLabel(rawFrame: number, efforts: readonly EffortLike[]): string {\n const effort = efforts[nearestEffortIndex(rawFrame, efforts)];\n return effort === undefined\n ? liangRankForFrame(rawFrame)\n : `${liangRankForFrame(rawFrame)} \u00B7 ${effort.name}`;\n}\n\nfunction lerp(a: number, b: number, amount: number): number {\n return a + (b - a) * amount;\n}\n\nfunction mix(a: Rgb, b: Rgb, amount: number): Rgb {\n return [\n Math.round(lerp(a[0], b[0], amount)),\n Math.round(lerp(a[1], b[1], amount)),\n Math.round(lerp(a[2], b[2], amount)),\n ];\n}\n\nfunction rgb(value: Rgb, alpha = 1): string {\n return alpha === 1\n ? `rgb(${value[0]} ${value[1]} ${value[2]})`\n : `rgb(${value[0]} ${value[1]} ${value[2]} / ${alpha})`;\n}\n\nexport function paletteForFrame(rawFrame: number): Palette {\n const frame = clampFrame(rawFrame);\n const level = (frame / PREVIEW_MAX_FRAME) * MAX_LEVEL;\n const portraitStage = Math.min(5, Math.floor(level / 6));\n const portraitFrom = STOPS[portraitStage];\n const portraitTo = STOPS[Math.min(5, portraitStage + 1)];\n const portraitAmount = portraitFrom === portraitTo\n ? 0\n : (level - portraitFrom.at) / (portraitTo.at - portraitFrom.at);\n\n // The UI itself is intentionally binary: levels 0\u201323 keep the native light\n // shell; entering the original \u6881\u795E/\u6881\u7956 region at level 24 switches black/gold.\n const dark = level >= 24;\n const stage = dark ? 5 : 0;\n const ui = dark ? STOPS[5] : STOPS[0];\n const page = ui.page;\n const surface = ui.surface;\n const surface2 = ui.surface2;\n const sidebar = mix(page, surface2, 0.25);\n const ink = ui.ink;\n const secondary = ui.secondary;\n // Keep Harness' native blue in the light shell. The original calibrator's\n // antique gold only becomes the product accent once the dark region begins.\n const accent: Rgb = dark ? [193, 154, 73] : [65, 118, 230];\n const accentHover = mix(accent, ink, 0.13);\n\n return {\n level,\n stage,\n strength: level / MAX_LEVEL,\n page: rgb(page),\n // Keep the shell readable while allowing the right-side portrait to remain\n // visibly present. Dense controls use the opaque layer tokens below.\n base: rgb(page, dark ? 0.42 : 0.28),\n layer1: rgb(surface, 0.94),\n layer2: rgb(surface2, 0.96),\n layer3: rgb(surface2, 0.99),\n sidebar: rgb(sidebar, 0.96),\n ink: rgb(ink),\n secondary: rgb(secondary),\n tertiary: rgb(mix(secondary, page, 0.28)),\n border: rgb(ink, dark ? 0.15 : 0.12),\n accent: rgb(accent),\n accentHover: rgb(accentHover),\n hover: rgb(ink, dark ? 0.09 : 0.07),\n portraitOpacity: String(lerp(\n portraitFrom.portraitOpacity,\n portraitTo.portraitOpacity,\n portraitAmount,\n )),\n };\n}\n"], + "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAMO;;;ACNP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA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", + "names": [] } diff --git a/package-lock.json b/package-lock.json index be3514e..d05d3fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@types/react": "~18.3.1", "esbuild": "^0.25.0", + "playwright-core": "^1.64.1", "vitest": "^4.1.10" } }, @@ -1330,6 +1331,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright-core": { + "version": "1.64.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz", + "integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", diff --git a/package.json b/package.json index 9cef8d4..79a169a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ ], "scripts": { "build": "node ./scripts/build-client.mjs", - "test": "vitest run" + "test": "vitest run", + "test:compatibility": "npm run build && node ./scripts/test-client-compatibility.mjs && node ./scripts/test-client-runtime.mjs" }, "dsh": { "bundle": { @@ -70,6 +71,7 @@ "devDependencies": { "@types/react": "~18.3.1", "esbuild": "^0.25.0", + "playwright-core": "^1.64.1", "vitest": "^4.1.10" } } diff --git a/scripts/test-client-compatibility.mjs b/scripts/test-client-compatibility.mjs new file mode 100644 index 0000000..0239f0b --- /dev/null +++ b/scripts/test-client-compatibility.mjs @@ -0,0 +1,280 @@ +import assert from "node:assert/strict"; +import { constants } from "node:fs"; +import { access, readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { inflateSync } from "node:zlib"; +import { build } from "esbuild"; +import { chromium } from "playwright-core"; + +// Self-contained host contracts; native Desktop acceptance remains manual. +const projectRoot = fileURLToPath(new URL("../", import.meta.url)); +const packageId = "@leon___/dsh-client-liang-intensity-skin"; +const viewport = { width: 1100, height: 700 }; +const css = await readFile(new URL("../src/client/skin.css", import.meta.url), "utf8"); +const compiled = await build({ absWorkingDir: projectRoot, entryPoints: ["src/client/appearance.ts"], + bundle: true, write: false, format: "iife", globalName: "LiangAppearance", platform: "browser", target: "es2022" }); + +async function chromeExecutable() { + const explicit = process.env.CHROME_PATH; + const candidates = explicit ? [explicit] : [ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/chromium", + "/usr/bin/chromium-browser", "/opt/google/chrome/chrome", + ]; + for (const candidate of candidates) { + try { await access(candidate, constants.X_OK); return candidate; } + catch { /* Use an existing local browser, never download one. */ } + } + throw new Error(`Chrome was not found. Set CHROME_PATH to an installed Chrome/Chromium executable. Checked: ${candidates.join(", ")}`); +} + +const portrait = `data:image/svg+xml,${encodeURIComponent('')}`; +function fixture({ desktop = false, top = 0, windows = false, mode = "compatibility" } = {}) { + return ` + + + +
+
+ `; +} + +async function prepare(context, options = {}) { + const page = await context.newPage(); + if (options.viewport) await page.setViewportSize(options.viewport); + const errors = []; + page.on("pageerror", error => errors.push(error.message)); + await page.setContent(fixture(options)); + await page.addScriptTag({ content: compiled.outputFiles[0].text }); + await page.evaluate(({ packageId, enabled }) => { + let snapshot = { enabled, bindEffort: true }; + const listeners = new Set(); + const calls = []; + let releaseChoice, holdNextChoice = false, failNextChoice = false, failNextBinding = false; + let dispose = () => {}; + const publish = patch => { + snapshot = { ...snapshot, ...patch }; + if (snapshot.enabled) document.body.dataset.liangSkin = "on"; + else delete document.body.dataset.liangSkin; + for (const listener of listeners) listener(); + }; + const scope = { + getSnapshot: () => snapshot, + subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, + async setBindEffort(value) { + calls.push(["binding", value]); + if (failNextBinding) { failNextBinding = false; throw new Error("fixture write rejected"); } + publish({ bindEffort: value }); + }, + }; + const presenter = { + async choose(value) { + calls.push(["choose", value]); + if (holdNextChoice) { holdNextChoice = false; await new Promise(resolve => { releaseChoice = resolve; }); } + if (failNextChoice) { failNextChoice = false; throw new Error("fixture choice rejected"); } + publish({ enabled: value }); + }, + }; + const markup = hash => `
${["light", "dark", "system"].map(theme => ``).join("")}
`; + const connectNative = () => { + for (const button of document.querySelectorAll("#native-row > button[data-theme]")) { + button.addEventListener("click", () => { window.fixture.nativeClicks++; window.fixture.nativeTheme = button.dataset.theme; }); + } + }; + window.fixture = { + calls, nativeClicks: 0, nativeTheme: null, portalClicks: 0, + snapshot: () => ({ ...snapshot }), listenerCount: () => listeners.size, + install(hash = "_8HJdBW") { + document.querySelector("#appearance-host").innerHTML = + '
' + + '
' + markup(hash); + connectNative(); + dispose = window.LiangAppearance.installLiangAppearanceButton(scope, presenter, packageId); + }, + rebuild(hash = "_newHash42") { + document.querySelector("#native-row").parentElement.outerHTML = markup(hash); connectNative(); + }, + removeRow() { document.querySelector("#native-row").parentElement.remove(); }, + restoreRow(hash = "_restored") { + document.querySelector("#appearance-host").insertAdjacentHTML("beforeend", markup(hash)); connectNative(); + }, + dispose() { dispose(); }, setEnabled(value) { publish({ enabled: value }); }, + holdChoice() { holdNextChoice = true; }, releaseChoice() { releaseChoice?.(); }, + rejectChoice() { failNextChoice = true; }, rejectBinding() { failNextBinding = true; }, + }; + document.querySelector("#portal").addEventListener("click", () => window.fixture.portalClicks++); + publish({}); + }, { packageId, enabled: options.enabled ?? true }); + await page.locator(".liang-skin-sequence-frame").evaluate(image => image.decode()); + return { page, errors }; +} + +// For the first pixel of a 1x1 PNG, every PNG filter's predictors are zero. +// Real rendered pixels catch opaque host layers that computed z-index misses. +async function pixelAt(page, x, y) { + const png = await page.screenshot({ clip: { x, y, width: 1, height: 1 }, animations: "disabled" }); + assert.equal(png.readUInt32BE(16), 1); assert.equal(png.readUInt32BE(20), 1); + assert.equal(png[24], 8, "Expected an 8-bit screenshot"); + assert.ok(png[25] === 2 || png[25] === 6, "Expected an RGB/RGBA screenshot"); + const chunks = []; + for (let offset = 8; offset < png.length;) { + const length = png.readUInt32BE(offset); + if (png.toString("ascii", offset + 4, offset + 8) === "IDAT") chunks.push(png.subarray(offset + 8, offset + 8 + length)); + offset += length + 12; + } + return [...inflateSync(Buffer.concat(chunks)).subarray(1, 4)]; +} +function closeColor(actual, expected, label) { + assert.ok(actual.every((channel, i) => Math.abs(channel - expected[i]) <= 1), `${label}: expected ${expected}, received ${actual}`); +} + +async function assertLayout(page, options, enabled = true) { + const size = page.viewportSize(); + const result = await page.evaluate(() => { + const root = document.querySelector("#root"), backdrop = document.querySelector(".liang-skin-backdrop"); + const box = element => { const rect = element.getBoundingClientRect(); return { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; }; + return { root: box(root), rootPosition: getComputedStyle(root).position, rootZ: getComputedStyle(root).zIndex, + backdrop: box(backdrop), portrait: box(backdrop.querySelector("img")), backdropZ: getComputedStyle(backdrop).zIndex, isolation: getComputedStyle(document.body).isolation, + bgBase: getComputedStyle(document.body).getPropertyValue("--dsw-alias-bg-base").trim(), + frameColor: getComputedStyle(document.querySelector(".fixture_frame")).backgroundColor }; + }); + assert.equal(result.rootPosition, options.desktop ? "fixed" : "static", "The host owns root positioning"); + assert.equal(result.rootZ, "auto", "The skin must not raise the root above native portals"); + const bounds = { x: 0, y: options.top ?? 0, width: size.width, height: size.height - (options.top ?? 0) }; + assert.deepEqual(result.root, bounds); + if (enabled) { + assert.equal(result.isolation, "isolate"); assert.equal(result.backdropZ, "-1"); + assert.equal(result.bgBase, "transparent", "Base surfaces must not apply a second portrait mask"); + assert.deepEqual(result.backdrop, bounds); + assert.equal(result.portrait.height, bounds.height, "Portrait follows the background container height"); + closeColor(await pixelAt(page, size.width - 30, 430), [24, 156, 72], "Portrait visible with exactly one 25% mask"); + if (options.windows) assert.equal(result.frameColor, "rgba(0, 0, 0, 0)", "Only the Windows outer frame becomes transparent"); + } else { + assert.equal(result.isolation, "auto", "Skin-off restores native stacking"); + assert.equal(result.bgBase, "rgb(240,244,248)"); + closeColor(await pixelAt(page, size.width - 30, 430), [240, 244, 248], "Skin-off native background"); + if (options.windows) assert.equal(result.frameColor, "rgb(11, 29, 49)"); + } + closeColor(await pixelAt(page, 100, 430), [11, 29, 49], "Sidebar keeps its fill"); + if ((options.top ?? 0) > 0) closeColor(await pixelAt(page, 500, 5), [90, 40, 80], "Native titlebar remains painted"); + await page.locator("#portal").click(); + assert.equal(await page.evaluate(() => window.fixture.portalClicks), enabled ? 1 : 2, "Native body portal remains clickable"); +} + +async function appearanceChecks(page, hash) { + await page.evaluate(hash => window.fixture.install(hash), hash); + const custom = page.locator("#native-row > .liang-appearance-choice"), binding = page.locator(".liang-appearance-binding__input"); + await custom.waitFor(); + assert.equal(await page.locator(".liang-appearance-choice").count(), 1); + assert.equal(await page.locator("#unrelated .liang-appearance-choice, #nested-unrelated .liang-appearance-choice").count(), 0); + assert.equal(await custom.getAttribute("data-plugin"), packageId); + assert.equal(await custom.getAttribute("aria-pressed"), "true"); + await binding.uncheck(); + await page.waitForFunction(() => !window.fixture.snapshot().bindEffort); + assert.equal(await binding.getAttribute("aria-checked"), "false"); + await binding.check(); await page.waitForFunction(() => window.fixture.snapshot().bindEffort); + for (const theme of ["light", "dark", "system"]) { + await page.locator(`[data-theme="${theme}"]`).click(); + await page.waitForFunction(() => !window.fixture.snapshot().enabled); + assert.equal(await page.locator(".liang-appearance-binding").count(), 0); + assert.equal(await custom.getAttribute("aria-pressed"), "false"); + assert.equal(await page.evaluate(() => window.fixture.nativeTheme), theme); + await custom.click(); await page.waitForFunction(() => window.fixture.snapshot().enabled); await binding.waitFor(); + } + assert.equal(await page.evaluate(() => window.fixture.nativeClicks), 3, "Native theme handlers still run"); + await page.evaluate(() => { + window.oldNative = document.querySelector('[data-theme="light"]'); + window.oldCustom = document.querySelector(".liang-appearance-choice"); + window.oldBinding = document.querySelector(".liang-appearance-binding__input"); window.fixture.rebuild(); + }); + await page.locator("#native-row._newHash42_cubeRow > .liang-appearance-choice").waitFor(); + assert.equal(await page.locator(".liang-appearance-choice").count(), 1, "DOM reconstruction produces one control"); + const beforeStaleClicks = await page.evaluate(() => window.fixture.calls.length); + await page.evaluate(() => { window.oldNative.click(); window.oldCustom.click(); window.oldBinding.dispatchEvent(new Event("change", { bubbles: true })); }); + assert.equal(await page.evaluate(() => window.fixture.calls.length), beforeStaleClicks, "Replaced nodes lose plugin listeners"); + await page.locator('[data-theme="dark"]').click(); await page.waitForFunction(() => !window.fixture.snapshot().enabled); + await custom.click(); await page.waitForFunction(() => window.fixture.snapshot().enabled); + await page.evaluate(() => window.fixture.removeRow()); + await page.waitForFunction(() => document.querySelector(".liang-appearance-choice") === null); + await page.evaluate(() => window.fixture.restoreRow()); await custom.waitFor(); + await page.evaluate(() => { + window.fixture.rejectBinding(); const input = document.querySelector(".liang-appearance-binding__input"); + input.checked = false; input.dispatchEvent(new Event("change", { bubbles: true })); + }); + await page.waitForFunction(() => document.querySelector(".liang-appearance-binding__input").checked); + await page.evaluate(() => window.fixture.rejectChoice()); await page.locator('[data-theme="light"]').click(); + await page.waitForFunction(() => !document.querySelector(".liang-appearance-choice").disabled); + assert.equal(await custom.getAttribute("aria-pressed"), "true", "Rejected preference preserves skin state"); + await page.evaluate(() => window.fixture.holdChoice()); await page.locator('[data-theme="light"]').click(); + assert.equal(await custom.isDisabled(), true, "Outstanding choices disable injected controls"); + await page.evaluate(() => { + window.oldCustom = document.querySelector(".liang-appearance-choice"); + window.fixture.dispose(); window.fixture.dispose(); window.fixture.releaseChoice(); + }); + await page.waitForFunction(() => !window.fixture.snapshot().enabled); + assert.equal(await page.locator(".liang-appearance-choice, .liang-appearance-binding").count(), 0, "Late choice cannot resurrect disposed controls"); + assert.equal(await page.evaluate(() => window.fixture.listenerCount()), 0); + const beforeDisposedClicks = await page.evaluate(() => window.fixture.calls.length); + await page.evaluate(() => { + window.oldCustom.click(); document.querySelector('[data-theme="dark"]').click(); + document.querySelector("#appearance-host").append(document.createElement("span")); + }); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(resolve))); + assert.equal(await page.evaluate(() => window.fixture.calls.length), beforeDisposedClicks); + assert.equal(await page.locator(".liang-appearance-choice, .liang-appearance-binding").count(), 0); +} + +const browser = await chromium.launch({ executablePath: await chromeExecutable(), headless: true }); +try { + const context = await browser.newContext({ viewport, reducedMotion: "reduce", deviceScaleFactor: 1 }); + await context.route("**/*", route => route.abort()); // No network needed by the fixture. + const cases = [ + { name: "Web static root" }, { name: "Desktop fixed top 0", desktop: true, top: 0 }, + { name: "Windows fixed top 36", desktop: true, windows: true, top: 36 }, + { name: "Windows extended top 40", desktop: true, windows: true, top: 40, mode: "extended" }, + { name: "macOS extended top 36", desktop: true, top: 36, mode: "extended" }, + { name: "macOS fixed top 40", desktop: true, top: 40 }, + { name: "Compact Desktop", desktop: true, windows: true, top: 36, viewport: { width: 800, height: 600 } }, + { name: "Large Web", viewport: { width: 1800, height: 1300 } }, + ]; + for (const options of cases) { + const { page, errors } = await prepare(context, options); + await assertLayout(page, options); await page.evaluate(() => window.fixture.setEnabled(false)); + await assertLayout(page, options, false); assert.deepEqual(errors, [], `${options.name}: browser errors`); + await page.close(); console.log(`PASS ${options.name}: backdrop, host geometry, native portal, skin off`); + } + for (const hash of ["_8HJdBW", "_currentHash89"]) { + const { page, errors } = await prepare(context); await appearanceChecks(page, hash); + assert.deepEqual(errors, [], `${hash}: browser errors`); await page.close(); + console.log(`PASS appearance ${hash}: multi-class discovery, choices, binding, DOM rebuild, disposal race`); + } + const first = await prepare(context), second = await prepare(context, { desktop: true, windows: true, top: 40 }); + await first.page.evaluate(() => window.fixture.install()); await second.page.evaluate(() => window.fixture.install("_otherWindow")); + await first.page.locator('[data-theme="light"]').click(); await first.page.waitForFunction(() => !window.fixture.snapshot().enabled); + assert.equal(await second.page.evaluate(() => window.fixture.snapshot().enabled), true); + closeColor(await pixelAt(first.page, 1070, 430), [240, 244, 248], "First window skin off"); + closeColor(await pixelAt(second.page, 1070, 430), [24, 156, 72], "Second window remains visible"); + assert.deepEqual([...first.errors, ...second.errors], []); await context.close(); + console.log("PASS independent windows: host geometry and appearance state stay isolated"); + console.log("Client compatibility browser checks passed (11 scenarios). Native Desktop validation remains manual."); +} finally { await browser.close(); } diff --git a/scripts/test-client-runtime.mjs b/scripts/test-client-runtime.mjs new file mode 100644 index 0000000..c82dd89 --- /dev/null +++ b/scripts/test-client-runtime.mjs @@ -0,0 +1,307 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { chromium } from 'playwright-core'; + +const root = new URL('../', import.meta.url); +const manifest = JSON.parse(readFileSync(new URL('package.json', root), 'utf8')); +const assetPaths = manifest.files.filter((file) => /^assets\/portrait-source-v2\/[^/]+\.webp$/.test(file)); +assert.equal(assetPaths.length, 24, 'The published client must contain all 24 portrait anchors'); +const assets = new Map(assetPaths.map((file) => [file.split('/').at(-1), readFileSync(new URL(file, root))])); +const client = readFileSync(new URL('lib/client.js', root), 'utf8'); +const imageSelector = '.liang-skin-backdrop[data-media="sequence"] > img'; +const browser = await chromium.launch({ + headless: true, + ...(process.env.CHROME_PATH + ? { executablePath: process.env.CHROME_PATH } + : process.platform === 'darwin' + ? { executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' } + : {}), +}); + +function deferred() { + let resolve; + const promise = new Promise((done) => { resolve = done; }); + return { promise, resolve }; +} + +async function bounded(promise, message) { + let timer; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), 15000); }), + ]); + } finally { + clearTimeout(timer); + } +} + +async function fixture({ fail = () => false, holdRetry = null } = {}) { + const context = await browser.newContext({ viewport: { width: 1200, height: 900 } }); + const page = await context.newPage(); + const errors = []; + const requests = []; + const retriesReleased = deferred(); + let closing = false; + let hold = null; + page.on('pageerror', (error) => errors.push(error.message)); + await page.route('http://liang.test/**', async (route) => { + const url = new URL(route.request().url()); + if (url.pathname === '/') { + await route.fulfill({ contentType: 'text/html', body: '
' }); + return; + } + const match = url.pathname.match(/^\/plugins\/.+\/assets\/portrait-source-v2\/([^/]+\.webp)$/); + assert.ok(match && assets.has(match[1]), `Unexpected runtime request: ${url.pathname}`); + const name = match[1]; + const retry = Number(url.searchParams.get('retry') ?? 0); + requests.push({ name, retry, url: url.href }); + const currentHold = hold; + if (currentHold !== null) { + currentHold.count += 1; + if (currentHold.count === assets.size) currentHold.started.resolve(); + await currentHold.release.promise; + } + if (name === holdRetry && retry > 0) await retriesReleased.promise; + const unavailable = fail(name, retry); + try { + await route.fulfill({ + status: unavailable ? 503 : 200, + contentType: 'image/webp', + headers: { 'cache-control': 'no-store' }, + body: unavailable ? Buffer.from('fixture: unavailable portrait') : assets.get(name), + }); + } catch (error) { + // Removing an Image's src during disposal may cancel its held request. + if (!closing) throw error; + } finally { + if (currentHold !== null) { + currentHold.finishedCount += 1; + if (currentHold.finishedCount === assets.size) currentHold.finished.resolve(); + } + } + }); + await page.goto('http://liang.test/'); + await page.evaluate(() => { + const runtime = { + cleanups: [], decoded: new WeakSet(), decodedImages: [], + registered: null, scope: null, presenter: null, module: null, + activeLocales: 0, activeSlots: 0, errors: [], + }; + window.__runtime = runtime; + window.addEventListener('unhandledrejection', (event) => runtime.errors.push(String(event.reason))); + // Observe successful native decodes without substituting Image, pixels, + // timing, or the browser's decode result. + const decode = HTMLImageElement.prototype.decode; + HTMLImageElement.prototype.decode = function (...args) { + return decode.apply(this, args).then((value) => { + runtime.decoded.add(this); + runtime.decodedImages.push(this); + return value; + }); + }; + const unexpectedRender = () => { throw new Error('The runtime fixture must not render React components'); }; + window.__ModuleLoader__ = { + load({ id, factory }) { + runtime.moduleId = id; + runtime.module = factory((name) => { + if (name === 'react') return { + useEffect: unexpectedRender, useMemo: unexpectedRender, + useRef: unexpectedRender, useState: unexpectedRender, + useSyncExternalStore: unexpectedRender, + }; + if (name === 'react/jsx-runtime') return { jsx: unexpectedRender, jsxs: unexpectedRender }; + throw new Error(`Unexpected client import: ${name}`); + }); + }, + }; + }); + await page.addScriptTag({ content: client }); + await page.evaluate(() => { + const runtime = window.__runtime; + let preference = 'system'; + const theme = { + getTheme: () => ({ preference }), + setTheme: (next) => { + preference = next; + document.body.toggleAttribute('data-ds-dark-theme', next === 'dark'); + }, + }; + const ctx = { + effect(setup) { + const cleanup = setup(); + if (typeof cleanup === 'function') runtime.cleanups.push(cleanup); + }, + get(name) { + if (name === 'theme') return theme; + throw new Error(`Unexpected host service: ${name}`); + }, + locale: { + register() { + runtime.activeLocales += 1; + return () => { runtime.activeLocales -= 1; }; + }, + }, + slots: { + inject(name, callback) { + const cleanup = callback(); + if (typeof cleanup === 'function') runtime.cleanups.push(cleanup); + }, + register(definition) { + runtime.activeSlots += 1; + runtime.registered = definition.inject('fixture-session'); + runtime.scope = runtime.registered.scope; + runtime.presenter = runtime.registered.presenter; + return () => { runtime.activeSlots -= 1; }; + }, + }, + sessions: { subagentAddress: () => undefined }, + modelDirectories: { + directoryFor: () => ({ + store: { getSnapshot: () => ({ current: null, groups: [], status: 'ready', error: null }), subscribe: () => () => {} }, + load: async () => {}, + select: async () => ({ ok: true }), + }), + }, + }; + if (typeof runtime.module?.apply !== 'function') throw new Error('Built client did not export apply(ctx)'); + runtime.module.apply(ctx); + runtime.dispose = () => { + for (const cleanup of runtime.cleanups.splice(0).reverse()) cleanup(); + }; + }); + assert.equal(await page.getAttribute('body', 'data-liang-skin'), 'on', 'apply(ctx) enables the real presenter'); + return { + page, requests, errors, + releaseRetries: () => retriesReleased.resolve(), + holdImages() { + hold = { count: 0, finishedCount: 0, started: deferred(), release: deferred(), finished: deferred() }; + return hold; + }, + async dispose() { + closing = true; + await page.evaluate(() => window.__runtime.dispose()); + }, + async close() { + closing = true; + hold?.release.resolve(); + retriesReleased.resolve(); + await context.close(); + }, + }; +} + +async function waitForPortrait(page, filename, retry = null) { + await page.waitForFunction(({ selector, filename, retry }) => { + const image = document.querySelector(selector); + if (!(image instanceof HTMLImageElement) || !image.currentSrc || !image.complete || image.naturalWidth === 0) return false; + const url = new URL(image.currentSrc); + return url.pathname.endsWith(`/${filename}`) + && (retry === null || url.searchParams.get('retry') === String(retry)) + && window.__runtime.decoded.has(image) + && getComputedStyle(image).display !== 'none' + && image.getBoundingClientRect().height > 0; + }, { selector: imageSelector, filename, retry }, { timeout: 15000 }); +} + +async function assertRemoved(page, fullyDisposed = false) { + const result = await page.evaluate(() => ({ + backdrop: document.querySelectorAll('.liang-skin-backdrop').length, + skin: document.body.hasAttribute('data-liang-skin'), + stage: document.body.hasAttribute('data-liang-stage'), + variables: [...document.body.style].filter((name) => name.startsWith('--liang-')), + retainedSources: window.__runtime.decodedImages.filter((image) => image.hasAttribute('src')).length, + styles: [...document.head.querySelectorAll('style')].filter((style) => style.dataset.plugin).length, + controls: document.querySelectorAll('.liang-appearance-choice, .liang-appearance-binding').length, + locales: window.__runtime.activeLocales, + slots: window.__runtime.activeSlots, + })); + assert.equal(result.backdrop, 0, 'No portrait backdrop remains'); + assert.equal(result.skin, false, 'Skin marker is removed'); + assert.equal(result.stage, false, 'Stage marker is removed'); + assert.deepEqual(result.variables, [], 'All presenter CSS variables are removed'); + assert.equal(result.retainedSources, 0, 'Decoded images release their network sources'); + if (fullyDisposed) { + assert.equal(result.styles, 0, 'Client stylesheet is removed'); + assert.equal(result.controls, 0, 'Appearance controls are removed'); + assert.equal(result.locales, 0, 'Locale registration is released'); + assert.equal(result.slots, 0, 'Slot registration is released'); + } +} + +async function assertNoErrors(test) { + assert.deepEqual(test.errors, [], 'No browser runtime exception'); + assert.deepEqual(await test.page.evaluate(() => window.__runtime.errors), [], 'No unhandled runtime promise rejection'); +} + +try { + const success = await fixture(); + try { + await success.page.waitForFunction(() => window.__runtime.decodedImages.length === 24, null, { timeout: 15000 }); + await waitForPortrait(success.page, 'stage-30.webp'); + assert.equal(success.requests.length, 24, 'Decoded display nodes do not start duplicate requests'); + assert.equal(new Set(success.requests.map(({ name }) => name)).size, 24, 'Every shipped anchor is requested'); + await success.page.evaluate(() => window.__runtime.presenter.setFrame(48)); + await waitForPortrait(success.page, 'stage-06.webp'); + await success.page.evaluate(() => window.__runtime.presenter.choose(false)); + await assertRemoved(success.page); + await success.page.evaluate(() => window.__runtime.presenter.choose(true)); + await waitForPortrait(success.page, 'stage-06.webp'); + await success.page.evaluate(() => window.__runtime.presenter.choose(false)); + const held = success.holdImages(); + await success.page.evaluate(() => window.__runtime.presenter.choose(true)); + await bounded(held.started.promise, 'Reactivation did not launch all 24 real image requests'); + await success.page.evaluate(() => window.__runtime.presenter.setFrame(240)); + await success.dispose(); + held.release.resolve(); + await bounded(held.finished.promise, 'Held image responses did not finish'); + await success.page.evaluate(async () => { + await window.__runtime.presenter.choose(true); + window.__runtime.presenter.setFrame(0); + window.__runtime.dispose(); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + }); + await assertRemoved(success.page, true); + await assertNoErrors(success); + } finally { + await success.close(); + } + + const partial = await fixture({ fail: (name) => name === 'stage-12.webp' || name === 'level-01.webp' }); + try { + await waitForPortrait(partial.page, 'stage-30.webp'); + const exhausted = partial.page.waitForResponse((response) => { + const url = new URL(response.url()); + return url.pathname.endsWith('/stage-12.webp') && url.searchParams.get('retry') === '3'; + }, { timeout: 15000 }); + await partial.page.evaluate(() => window.__runtime.presenter.setFrame(96)); + await exhausted; + await partial.page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await waitForPortrait(partial.page, 'stage-30.webp'); + assert.deepEqual(partial.requests.filter(({ name }) => name === 'stage-12.webp').map(({ retry }) => retry), [0, 1, 2, 3], 'A permanently failed target gets bounded cache-bypassing retries'); + await partial.dispose(); + await assertRemoved(partial.page, true); + await assertNoErrors(partial); + } finally { + await partial.close(); + } + + const recovery = await fixture({ fail: (name, retry) => name === 'stage-00.webp' && retry === 0, holdRetry: 'stage-00.webp' }); + try { + await waitForPortrait(recovery.page, 'stage-30.webp'); + await recovery.page.evaluate(() => window.__runtime.presenter.setFrame(0)); + await recovery.page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))); + await waitForPortrait(recovery.page, 'stage-30.webp'); + recovery.releaseRetries(); + await waitForPortrait(recovery.page, 'stage-00.webp', 1); + assert.deepEqual(recovery.requests.filter(({ name }) => name === 'stage-00.webp').map(({ retry }) => retry), [0, 1], 'First-frame recovery retries the actual WebP request'); + await recovery.dispose(); + await assertRemoved(recovery.page, true); + await assertNoErrors(recovery); + } finally { + await recovery.close(); + } + console.log('PASS: built apply(ctx), 24 native WebP decodes, frame swaps, partial failure, first-frame retry, disable/enable, and disposal with late responses.'); +} finally { + await browser.close(); +} diff --git a/src/client/appearance.ts b/src/client/appearance.ts new file mode 100644 index 0000000..94c2036 --- /dev/null +++ b/src/client/appearance.ts @@ -0,0 +1,208 @@ +export interface AppearanceSnapshot { + enabled: boolean; + bindEffort: boolean; +} + +export interface AppearanceScope { + getSnapshot(): AppearanceSnapshot; + subscribe(listener: () => void): () => void; + setBindEffort(enabled: boolean): Promise; +} + +export interface AppearancePresenter { + choose(enabled: boolean): Promise; +} + +const NATIVE_APPEARANCE_ROW = '[class*="_cubeRow"]'; +const LIANG_APPEARANCE_BUTTON = "liang-appearance-choice"; +const LIANG_BINDING_CONTROL = "liang-appearance-binding"; +const LIANG_BINDING_INPUT = "liang-appearance-binding__input"; + +function hasClassSuffix(node: Element, suffix: string) { + return [...node.classList].some((name) => name.endsWith(suffix)); +} + +function findAppearanceRow() { + for (const row of document.querySelectorAll(NATIVE_APPEARANCE_ROW)) { + if (!hasClassSuffix(row, "_cubeRow")) continue; + // All three native appearance choices are direct button children. Validate + // the structure as well as the suffix so unrelated cube rows stay alone. + const nativeButtons = [...row.children].filter( + (node): node is HTMLButtonElement => node instanceof HTMLButtonElement + && hasClassSuffix(node, "_themeCube"), + ); + if (nativeButtons.length < 3) continue; + + let group = row.parentElement; + while (group !== null && !hasClassSuffix(group, "_group")) { + group = group.parentElement; + } + group ??= row.parentElement; + if (group !== null) return { row, group, nativeButtons }; + } + return null; +} + +interface MountedAppearance { + row: HTMLElement; + group: HTMLElement; + button: HTMLButtonElement; + binding: HTMLLabelElement; + bindingInput: HTMLInputElement; + handleCustomClick: () => void; + handleBindingChange: () => void; +} + +export function installLiangAppearanceButton( + scope: AppearanceScope, + presenter: AppearancePresenter, + packageId: string, +): () => void { + let pending = false; + let disposed = false; + let mounted: MountedAppearance | null = null; + const nativeClickHandlers = new Map void>(); + + const unmount = () => { + for (const [button, handler] of nativeClickHandlers) { + button.removeEventListener("click", handler, { capture: true }); + } + nativeClickHandlers.clear(); + if (mounted === null) return; + mounted.button.removeEventListener("click", mounted.handleCustomClick); + mounted.bindingInput.removeEventListener("change", mounted.handleBindingChange); + mounted.button.remove(); + mounted.binding.remove(); + mounted = null; + }; + + const choose = async (enabled: boolean) => { + if (disposed || pending || scope.getSnapshot().enabled === enabled) return; + pending = true; + sync(); + try { + await presenter.choose(enabled); + } catch { + // The preference store remains the source of truth after a failed choice. + } finally { + pending = false; + if (!disposed) sync(); + } + }; + + const createMount = (row: HTMLElement, group: HTMLElement): MountedAppearance => { + const english = document.documentElement.lang.toLowerCase().startsWith("en"); + const button = document.createElement("button"); + button.className = LIANG_APPEARANCE_BUTTON; + button.type = "button"; + button.dataset.plugin = packageId; + button.setAttribute("aria-label", "滑动变祖"); + + const icon = document.createElement("span"); + icon.className = "liang-appearance-choice__icon"; + icon.setAttribute("aria-hidden", "true"); + icon.textContent = "◈"; + + const label = document.createElement("span"); + label.className = "liang-appearance-choice__label"; + label.textContent = english ? "Slider" : "滑动变祖"; + button.append(icon, label); + const handleCustomClick = () => { void choose(true); }; + button.addEventListener("click", handleCustomClick); + + const binding = document.createElement("label"); + binding.className = LIANG_BINDING_CONTROL; + binding.dataset.plugin = packageId; + + const bindingCopy = document.createElement("span"); + bindingCopy.className = "liang-appearance-binding__copy"; + const bindingLabel = document.createElement("span"); + bindingLabel.className = "liang-appearance-binding__label"; + const bindingText = english ? "Bind slider to reasoning level" : "滑动变祖绑定思考等级"; + bindingLabel.textContent = bindingText; + + const bindingDescription = document.createElement("span"); + bindingDescription.className = "liang-appearance-binding__description"; + bindingDescription.id = "liang-appearance-binding-description"; + bindingDescription.textContent = english + ? "When off, the slider does not change the reasoning level." + : "关闭之后滑块不联动思考等级"; + bindingCopy.append(bindingLabel, bindingDescription); + + const bindingInput = document.createElement("input"); + bindingInput.className = LIANG_BINDING_INPUT; + bindingInput.type = "checkbox"; + bindingInput.setAttribute("role", "switch"); + bindingInput.setAttribute("aria-label", bindingText); + bindingInput.setAttribute("aria-describedby", bindingDescription.id); + const updateBinding = async () => { + if (disposed || pending) return; + try { + await scope.setBindEffort(bindingInput.checked); + } catch { + // Restore the persisted selection if writing the preference fails. + } finally { + if (!disposed) sync(); + } + }; + const handleBindingChange = () => { void updateBinding(); }; + bindingInput.addEventListener("change", handleBindingChange); + binding.append(bindingCopy, bindingInput); + + return { row, group, button, binding, bindingInput, handleCustomClick, handleBindingChange }; + }; + + const sync = () => { + if (disposed) return; + const target = findAppearanceRow(); + if (target === null) { + unmount(); + return; + } + if (mounted !== null && (mounted.row !== target.row || mounted.group !== target.group)) { + unmount(); + } + mounted ??= createMount(target.row, target.group); + const { row, group, button, binding, bindingInput } = mounted; + if (button.parentElement !== row) row.append(button); + + const snapshot = scope.getSnapshot(); + button.disabled = pending; + button.setAttribute("aria-pressed", String(snapshot.enabled)); + if (snapshot.enabled) { + if (binding.parentElement !== group) group.append(binding); + bindingInput.checked = snapshot.bindEffort; + bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort)); + bindingInput.disabled = pending; + } else { + binding.remove(); + } + + for (const [nativeButton, handler] of nativeClickHandlers) { + if (target.nativeButtons.includes(nativeButton)) continue; + nativeButton.removeEventListener("click", handler, { capture: true }); + nativeClickHandlers.delete(nativeButton); + } + for (const nativeButton of target.nativeButtons) { + if (nativeClickHandlers.has(nativeButton)) continue; + const handleNativeClick = () => { void choose(false); }; + nativeClickHandlers.set(nativeButton, handleNativeClick); + nativeButton.addEventListener("click", handleNativeClick, { capture: true }); + } + }; + + const observer = new MutationObserver(sync); + observer.observe(document.body, { childList: true, subtree: true }); + const unsubscribe = scope.subscribe(sync); + sync(); + + return () => { + if (disposed) return; + // Mark disposal first: outstanding choices and queued observer callbacks + // must never attach new controls after the client has been deactivated. + disposed = true; + observer.disconnect(); + unsubscribe(); + unmount(); + }; +} diff --git a/src/client/index.tsx b/src/client/index.tsx index ec62b3f..a01232b 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -11,13 +11,14 @@ import type {} from "@deepseek-ai/dsh-client-ui-conversation/client"; import type {} from "@deepseek-ai/dsh-client-ui-settings/client"; import type {} from "@deepseek-ai/dsh-client-ui-model-selection/client"; import styles from "./skin.css"; +import { installLiangAppearanceButton } from "./appearance"; +import { createPortraitLoader } from "./portrait-loader"; import { PREVIEW_MAX_FRAME, frameForEffort, indicatorLabel, nearestEffortIndex, paletteForFrame, - portraitBlendForLevel, selectedEffortIndex, type EffortLike, } from "./logic"; @@ -25,7 +26,6 @@ import { const PACKAGE_ID = "dsh-client-liang-intensity-skin"; const LOCALE_NAMESPACE = "liang.skin"; const ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`; -const FIRST_PORTRAIT_FILE = "portrait-source-v2/stage-00.webp"; const BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort"; const PORTRAIT_ANCHORS = [ @@ -55,8 +55,6 @@ const PORTRAIT_ANCHORS = [ { level: 30, file: "stage-30.webp" }, ] as const; -const ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level); - interface SkinSettings { enabled: boolean; bindEffort: boolean; @@ -142,9 +140,7 @@ class SkinPresenter { private readonly scope: PreferenceStore; private readonly theme: ThemeService; private readonly root: HTMLDivElement; - private readonly portrait: HTMLImageElement; - private readonly preloads: HTMLImageElement[]; - private portraitReady = false; + private readonly portraits: ReturnType; private enabled = true; // Default to the max frame so the first paint after load is the dark shell; // starting at 0 flashed the light palette before the directory resolved. @@ -160,62 +156,34 @@ class SkinPresenter { this.root = document.createElement("div"); this.root.className = "liang-skin-backdrop"; this.root.dataset.plugin = PACKAGE_ID; - // Show the first half-body frame immediately while the remaining frames - // are fetched and decoded. A request being complete does not mean the - // bitmap is ready for a tear-free first swap. - this.root.dataset.media = "sequence"; + this.root.dataset.media = "color"; this.root.setAttribute("aria-hidden", "true"); - - this.portrait = document.createElement("img"); - this.portrait.className = "liang-skin-sequence-frame"; - this.portrait.alt = ""; - this.portrait.draggable = false; - this.portrait.decoding = "async"; - this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`; - this.portrait.addEventListener("error", this.handleSequenceError); - - this.preloads = PORTRAIT_ANCHORS.map(({ file }) => { - const image = new Image(); - image.loading = "eager"; - image.decoding = "async"; - image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`; - return image; - }); - - // `new Image()` starts the requests, but the browser may still defer - // decoding until the image is attached to the document. Wait for all - // frames up front so the first slider interaction never reveals a blank - // or half-painted frame. - void Promise.all(this.preloads.map((image) => image.decode())).then( - () => { - if (this.disposed) return; - this.portraitReady = true; + this.portraits = createPortraitLoader({ + sources: PORTRAIT_ANCHORS.map(({ level, file }) => ({ + level, + url: `${ASSET_PREFIX}/portrait-source-v2/${file}`, + })), + onChange: ({ image }) => { + if (this.disposed || !this.enabled) return; + if (image === null) { + this.root.dataset.media = "color"; + this.root.replaceChildren(); + return; + } + // Reuse the decoded node so a frame swap cannot start a second fetch. + const portrait = image as HTMLImageElement; + portrait.className = "liang-skin-sequence-frame"; + portrait.alt = ""; + portrait.draggable = false; + if (this.root.firstChild !== portrait) this.root.replaceChildren(portrait); this.root.dataset.media = "sequence"; - this.updatePortrait(paletteForFrame(this.frame).level); }, - () => { - // Keep the already-visible first half-body frame if another optional - // frame cannot be decoded. It is still a valid skin fallback. - }, - ); - - this.root.append(this.portrait); - document.body.prepend(this.root); + }); this.unsubscribe = scope.subscribe(() => this.syncSettings()); this.syncSettings(); } - private readonly handleSequenceError = () => { - if (this.portrait.src.endsWith(FIRST_PORTRAIT_FILE)) { - this.root.dataset.media = "color"; - return; - } - this.portraitReady = false; - this.portrait.src = `${ASSET_PREFIX}/${FIRST_PORTRAIT_FILE}`; - this.root.dataset.media = "sequence"; - }; - private syncSettings() { this.setEnabled(this.scope.getSnapshot().enabled); } @@ -229,13 +197,20 @@ class SkinPresenter { } setEnabled(enabled: boolean) { + if (this.disposed) return; this.enabled = enabled; if (enabled) { if (!this.root.isConnected) document.body.prepend(this.root); document.body.dataset.liangSkin = "on"; this.applyFrame(); + this.portraits.start(); } else { + if (this.raf !== 0) cancelAnimationFrame(this.raf); + this.raf = 0; this.root.remove(); + this.portraits.stop(); + this.root.replaceChildren(); + this.root.dataset.media = "color"; delete document.body.dataset.liangSkin; delete document.body.dataset.liangStage; for (const name of cssVariables) document.body.style.removeProperty(name); @@ -243,6 +218,7 @@ class SkinPresenter { } setFrame(frame: number) { + if (this.disposed) return; this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame))); this.frame = this.pendingFrame; if (!this.enabled) return; @@ -255,6 +231,7 @@ class SkinPresenter { } private applyFrame() { + if (this.disposed || !this.enabled) return; const palette = paletteForFrame(this.frame); const body = document.body; this.syncNativeTheme(palette.stage === 5 ? "dark" : "light"); @@ -283,16 +260,7 @@ class SkinPresenter { } private updatePortrait(level: number) { - if (!this.portraitReady) return; - const { lowerIndex, upperIndex, mix } = portraitBlendForLevel( - level, - ANCHOR_LEVELS, - ); - const lower = PORTRAIT_ANCHORS[lowerIndex]; - const upper = PORTRAIT_ANCHORS[upperIndex]; - const selected = mix >= 0.5 ? upper : lower; - const source = `${ASSET_PREFIX}/portrait-source-v2/${selected.file}`; - if (this.portrait.getAttribute("src") !== source) this.portrait.src = source; + this.portraits.select(level); } async choose(enabled: boolean) { @@ -310,8 +278,7 @@ class SkinPresenter { this.disposed = true; this.unsubscribe(); if (this.raf !== 0) cancelAnimationFrame(this.raf); - this.portrait.removeEventListener("error", this.handleSequenceError); - for (const image of this.preloads) image.src = ""; + this.portraits.dispose(); this.root.remove(); document.body.removeAttribute("data-liang-skin"); delete document.body.dataset.liangStage; @@ -590,141 +557,6 @@ function AppearanceSkinRow({ scope, presenter, theme, subscribeTheme, t }: Setti ); } -const NATIVE_APPEARANCE_GROUP = '[class*="_8HJdBW_group"]'; -const NATIVE_APPEARANCE_ROW = '[class*="_8HJdBW_cubeRow"]'; -const LIANG_APPEARANCE_BUTTON = "liang-appearance-choice"; -const LIANG_BINDING_CONTROL = "liang-appearance-binding"; -const LIANG_BINDING_INPUT = "liang-appearance-binding__input"; - -function installLiangAppearanceButton(scope: PreferenceStore, presenter: SkinPresenter) { - let pending = false; - const hookedNativeButtons = new Set(); - const nativeClickHandlers = new Map void>(); - - const sync = () => { - const group = [...document.querySelectorAll(NATIVE_APPEARANCE_GROUP)] - .find((node) => node.querySelector('[class*="_8HJdBW_themeCube"]')); - const row = group?.querySelector(NATIVE_APPEARANCE_ROW); - if (row === undefined || row === null) return; - - let customButton = row.querySelector(`.${LIANG_APPEARANCE_BUTTON}`); - if (customButton === null) { - customButton = document.createElement("button"); - customButton.className = LIANG_APPEARANCE_BUTTON; - customButton.type = "button"; - customButton.dataset.plugin = PACKAGE_ID; - customButton.setAttribute("aria-label", "滑动变祖"); - - const icon = document.createElement("span"); - icon.className = "liang-appearance-choice__icon"; - icon.setAttribute("aria-hidden", "true"); - icon.textContent = "◈"; - - const label = document.createElement("span"); - label.className = "liang-appearance-choice__label"; - label.textContent = document.documentElement.lang.toLowerCase().startsWith("en") - ? "Slider" - : "滑动变祖"; - customButton.append(icon, label); - customButton.addEventListener("click", () => { - if (pending || scope.getSnapshot().enabled) return; - pending = true; - sync(); - void presenter.choose(true).finally(() => { - pending = false; - sync(); - }); - }); - } - - if (customButton.parentElement !== row) row.append(customButton); - const snapshot = scope.getSnapshot(); - customButton.disabled = pending; - customButton.setAttribute("aria-pressed", String(snapshot.enabled)); - - let bindingControl = group.querySelector(`.${LIANG_BINDING_CONTROL}`); - if (!snapshot.enabled) { - bindingControl?.remove(); - bindingControl = null; - } else if (bindingControl === null) { - bindingControl = document.createElement("label"); - bindingControl.className = LIANG_BINDING_CONTROL; - bindingControl.dataset.plugin = PACKAGE_ID; - - const bindingCopy = document.createElement("span"); - bindingCopy.className = "liang-appearance-binding__copy"; - - const bindingLabel = document.createElement("span"); - bindingLabel.className = "liang-appearance-binding__label"; - const bindingText = document.documentElement.lang.toLowerCase().startsWith("en") - ? "Bind slider to reasoning level" - : "滑动变祖绑定思考等级"; - bindingLabel.textContent = bindingText; - - const bindingDescription = document.createElement("span"); - bindingDescription.className = "liang-appearance-binding__description"; - bindingDescription.id = "liang-appearance-binding-description"; - bindingDescription.textContent = document.documentElement.lang.toLowerCase().startsWith("en") - ? "When off, the slider does not change the reasoning level." - : "关闭之后滑块不联动思考等级"; - bindingCopy.append(bindingLabel, bindingDescription); - - const bindingInput = document.createElement("input"); - bindingInput.className = LIANG_BINDING_INPUT; - bindingInput.type = "checkbox"; - bindingInput.setAttribute("role", "switch"); - bindingInput.setAttribute("aria-label", bindingText); - bindingInput.setAttribute("aria-describedby", bindingDescription.id); - bindingInput.addEventListener("change", () => { - void scope.setBindEffort(bindingInput.checked).catch(() => sync()); - }); - - bindingControl.append(bindingCopy, bindingInput); - group.append(bindingControl); - } - - if (bindingControl !== null) { - const bindingInput = bindingControl.querySelector(`.${LIANG_BINDING_INPUT}`); - if (bindingInput !== null) { - bindingInput.checked = snapshot.bindEffort; - bindingInput.setAttribute("aria-checked", String(snapshot.bindEffort)); - bindingInput.disabled = pending; - } - } - - for (const nativeButton of row.querySelectorAll('[class*="_8HJdBW_themeCube"]')) { - if (hookedNativeButtons.has(nativeButton)) continue; - hookedNativeButtons.add(nativeButton); - const handleNativeClick = () => { - if (pending || !scope.getSnapshot().enabled) return; - pending = true; - sync(); - void presenter.choose(false).finally(() => { - pending = false; - sync(); - }); - }; - nativeClickHandlers.set(nativeButton, handleNativeClick); - nativeButton.addEventListener("click", handleNativeClick, { capture: true }); - } - }; - - const observer = new MutationObserver(sync); - observer.observe(document.body, { childList: true, subtree: true }); - const unsubscribe = scope.subscribe(sync); - sync(); - - return () => { - observer.disconnect(); - unsubscribe(); - for (const [nativeButton, handleNativeClick] of nativeClickHandlers) { - nativeButton.removeEventListener("click", handleNativeClick, { capture: true }); - } - document.querySelectorAll(`.${LIANG_APPEARANCE_BUTTON}`).forEach((button) => button.remove()); - document.querySelectorAll(`.${LIANG_BINDING_CONTROL}`).forEach((control) => control.remove()); - }; -} - export const inject = [ "slots", "sessions", @@ -795,7 +627,7 @@ export function apply(ctx: ClientContext) { const presenter = new SkinPresenter(scope, theme); ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter"); ctx.effect( - () => installLiangAppearanceButton(scope, presenter), + () => installLiangAppearanceButton(scope, presenter, PACKAGE_ID), "liang-intensity-skin: native appearance extension", ); ctx.effect( diff --git a/src/client/portrait-loader.test.ts b/src/client/portrait-loader.test.ts new file mode 100644 index 0000000..27d932e --- /dev/null +++ b/src/client/portrait-loader.test.ts @@ -0,0 +1,214 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createPortraitLoader, type PortraitImage, type PortraitState } from "./portrait-loader"; + +class FakeImage implements PortraitImage { + loading: "eager" | "lazy" = "eager"; + decoding: "async" | "sync" | "auto" = "async"; + private source = ""; + readonly requests: string[] = []; + readonly listeners = new Map void>>(); + resolveDecode!: () => void; + rejectDecode!: (reason?: unknown) => void; + private readonly decoded = new Promise((resolve, reject) => { + this.resolveDecode = resolve; + this.rejectDecode = reject; + }); + get src() { return this.source; } + set src(value: string) { this.source = value; this.requests.push(value); } + decode() { return this.decoded; } + addEventListener(type: "load" | "error", listener: () => void) { + if (!this.listeners.has(type)) this.listeners.set(type, new Set()); + this.listeners.get(type)!.add(listener); + } + removeEventListener(type: "load" | "error", listener: () => void) { + this.listeners.get(type)?.delete(listener); + } + removeAttribute(name: string) { if (name === "src") this.source = ""; } + emit(type: "load" | "error") { + for (const listener of [...(this.listeners.get(type) ?? [])]) listener(); + } + async succeed() { + this.emit("load"); + this.resolveDecode(); + await Promise.resolve(); + } + listenerCount() { + return [...this.listeners.values()].reduce((total, entries) => total + entries.size, 0); + } +} + +function harness(urls = ["/portrait-0.webp", "/portrait-10.webp", "/portrait-20.webp"]) { + const images: FakeImage[] = []; + const states: PortraitState[] = []; + const loader = createPortraitLoader({ + sources: urls.map((url, index) => ({ level: index * 10, url })), + createImage: () => { + const image = new FakeImage(); + images.push(image); + return image; + }, + onChange: (state) => states.push(state), + }); + return { loader, images, states, state: () => states.at(-1) }; +} + +beforeEach(() => vi.useFakeTimers()); +afterEach(() => vi.useRealTimers()); + +describe("portrait loading recovery", () => { + it("shows each decoded frame without waiting for other requests or decodes", async () => { + const { loader, images, state } = harness(); + loader.select(20); + loader.start(); + images[0].emit("load"); // Leave this decode pending. + await images[1].succeed(); + expect(state()).toEqual({ index: 1, url: "/portrait-10.webp", image: images[1], status: "ready" }); + images[2].emit("error"); + await vi.advanceTimersByTimeAsync(500); + await images[3].succeed(); + expect(state()).toEqual({ index: 2, url: "/portrait-20.webp?retry=1", image: images[3], status: "ready" }); + loader.dispose(); + }); + + it("keeps the displayed frame until the requested target becomes available", async () => { + const { loader, images, state } = harness(); + loader.start(); + await images[0].succeed(); + loader.select(20); + await images[1].succeed(); + expect(state()?.index).toBe(0); + await images[2].succeed(); + expect(state()?.index).toBe(2); + loader.select(5); // Midpoint follows the upper anchor, like portraitBlendForLevel. + expect(state()?.index).toBe(1); + loader.dispose(); + }); + + it("limits each URL to three retries with the required backoff and cache keys", async () => { + const { loader, images, state } = harness(["/portrait.webp?size=2#frame"]); + loader.start(); + images[0].emit("error"); + for (const [attempt, delay] of [500, 1500, 3000].entries()) { + await vi.advanceTimersByTimeAsync(delay - 1); + expect(images).toHaveLength(attempt + 1); + await vi.advanceTimersByTimeAsync(1); + expect(images[attempt + 1].src).toBe(`/portrait.webp?size=2&retry=${attempt + 1}#frame`); + images[attempt + 1].emit("error"); + } + expect(state()).toEqual({ index: null, url: null, image: null, status: "failed" }); + await vi.runAllTimersAsync(); + expect(images).toHaveLength(4); + expect(images.every((image) => image.src === "" && image.listenerCount() === 0)).toBe(true); + loader.dispose(); + }); + + it("falls back to color only after all frame requests exhaust their retries", async () => { + const { loader, images, state } = harness(["/a.webp", "/b.webp"]); + loader.start(); + const failPending = () => images.filter((image) => image.src).forEach((image) => image.emit("error")); + failPending(); + expect(state()?.status).toBe("loading"); + for (const delay of [500, 1500, 3000]) { + await vi.advanceTimersByTimeAsync(delay); + failPending(); + } + expect(images).toHaveLength(8); + expect(state()).toEqual({ index: null, url: null, image: null, status: "failed" }); + loader.dispose(); + }); + + it("preserves a usable frame when another URL exhausts every retry", async () => { + const { loader, images, state } = harness(["/a.webp", "/b.webp"]); + loader.select(10); + loader.start(); + await images[0].succeed(); + images[1].emit("error"); + for (const [attempt, delay] of [500, 1500, 3000].entries()) { + await vi.advanceTimersByTimeAsync(delay); + images[attempt + 2].emit("error"); + } + expect(state()).toEqual({ index: 0, url: "/a.webp", image: images[0], status: "ready" }); + loader.dispose(); + }); + + it("retries a decode failure and accepts a recovered target frame", async () => { + const { loader, images, state } = harness(["/a.webp"]); + loader.start(); + images[0].emit("load"); + images[0].rejectDecode(new Error("incomplete bitmap")); + await Promise.resolve(); + await vi.advanceTimersByTimeAsync(500); + await images[1].succeed(); + expect(state()).toEqual({ index: 0, url: "/a.webp?retry=1", image: images[1], status: "ready" }); + loader.dispose(); + }); + + it("cancels retries and releases ready, decoding, and loading images on stop", async () => { + const { loader, images, states } = harness(); + loader.start(); + await images[0].succeed(); + images[1].emit("load"); + images[2].emit("error"); + const beforeStop = states.length; + loader.stop(); + expect(images.every((image) => image.src === "" && image.listenerCount() === 0)).toBe(true); + images[1].resolveDecode(); + await vi.runAllTimersAsync(); + expect(images).toHaveLength(3); + expect(states).toHaveLength(beforeStop); + loader.dispose(); + }); + + it("rejects stale decode results after stop and restart", async () => { + const { loader, images, states, state } = harness(["/a.webp"]); + loader.start(); + images[0].emit("load"); + loader.stop(); + loader.start(); + const restartedStates = states.length; + images[0].resolveDecode(); + await Promise.resolve(); + expect(states).toHaveLength(restartedStates); + expect(state()?.status).toBe("loading"); + await images[1].succeed(); + expect(state()?.status).toBe("ready"); + loader.dispose(); + }); + + it("makes disposal permanent and ignores saved callbacks from cancelled attempts", async () => { + const { loader, images, states } = harness(["/a.webp"]); + loader.start(); + const staleLoad = [...images[0].listeners.get("load")!][0]; + const staleError = [...images[0].listeners.get("error")!][0]; + loader.dispose(); + const disposedStates = states.length; + staleLoad(); + staleError(); + loader.select(10); + loader.start(); + loader.dispose(); + await vi.runAllTimersAsync(); + expect(images).toHaveLength(1); + expect(images[0].src).toBe(""); + expect(states).toHaveLength(disposedStates); + }); + + it("loads duplicate source URLs once while selecting their anchor levels", async () => { + const { loader, images, state } = harness(["/shared.webp", "/shared.webp"]); + loader.select(10); + loader.start(); + loader.start(); + expect(images).toHaveLength(1); + await images[0].succeed(); + expect(state()).toEqual({ index: 1, url: "/shared.webp", image: images[0], status: "ready" }); + loader.dispose(); + }); + + it("handles an empty source list without creating image requests", () => { + const { loader, images, state } = harness([]); + loader.start(); + expect(images).toHaveLength(0); + expect(state()).toEqual({ index: null, url: null, image: null, status: "failed" }); + loader.dispose(); + }); +}); diff --git a/src/client/portrait-loader.ts b/src/client/portrait-loader.ts new file mode 100644 index 0000000..732e137 --- /dev/null +++ b/src/client/portrait-loader.ts @@ -0,0 +1,201 @@ +export interface PortraitSource { + level: number; + url: string; +} + +export interface PortraitState { + index: number | null; + url: string | null; + status: "loading" | "ready" | "failed"; + image: PortraitImage | null; +} + +export interface PortraitImage { + src: string; + loading: "eager" | "lazy"; + decoding: "async" | "sync" | "auto"; + decode(): Promise; + addEventListener(type: "load" | "error", listener: () => void): void; + removeEventListener(type: "load" | "error", listener: () => void): void; + removeAttribute(name: string): void; +} + +export interface PortraitLoaderOptions { + sources: readonly PortraitSource[]; + onChange: (state: PortraitState) => void; + createImage?: () => PortraitImage; +} + +interface Asset { + url: string; + status: "idle" | "loading" | "ready" | "failed"; + loadedUrl: string | null; + retries: number; + image: PortraitImage | null; + timer: ReturnType | null; + detach: (() => void) | null; +} + +const RETRY_DELAYS = [500, 1500, 3000] as const; + +function retryUrl(source: string, retry: number) { + if (retry === 0) return source; + const hashIndex = source.indexOf("#"); + const hash = hashIndex < 0 ? "" : source.slice(hashIndex); + const base = hashIndex < 0 ? source : source.slice(0, hashIndex); + const queryIndex = base.indexOf("?"); + const path = queryIndex < 0 ? base : base.slice(0, queryIndex); + const query = new URLSearchParams(queryIndex < 0 ? "" : base.slice(queryIndex + 1)); + query.set("retry", String(retry)); + return `${path}?${query}${hash}`; +} + +/** + * Decode frames independently and return the decoded image itself for display. + * start() resumes after stop(); stop() silently releases every request/image. + * dispose() is permanent. select() also accepts a target before start(). + */ +export function createPortraitLoader(options: PortraitLoaderOptions) { + const sources = options.sources.map((source) => ({ ...source })); + const assets = new Map(); + for (const { url } of sources) { + if (!assets.has(url)) { + assets.set(url, { + url, status: "idle", loadedUrl: null, retries: 0, + image: null, timer: null, detach: null, + }); + } + } + const createImage = options.createImage ?? (() => new Image()); + let onChange: PortraitLoaderOptions["onChange"] | null = options.onChange; + let active = false; + let disposed = false; + let generation = 0; + let targetLevel = sources[0]?.level ?? 0; + let displayedIndex: number | null = null; + let lastState: PortraitState | null = null; + + const isCurrent = (run: number) => active && !disposed && generation === run; + const nearestIndex = (available: (index: number) => boolean) => { + let nearest: number | null = null; + for (let index = 0; index < sources.length; index += 1) { + if (!available(index)) continue; + const distance = Math.abs(sources[index].level - targetLevel); + const previous = nearest === null ? Infinity : Math.abs(sources[nearest].level - targetLevel); + if (nearest === null || distance < previous || (distance === previous && sources[index].level > sources[nearest].level)) { + nearest = index; + } + } + return nearest; + }; + const isReady = (index: number) => assets.get(sources[index].url)?.status === "ready"; + const emit = (state: PortraitState) => { + if (lastState?.index === state.index && lastState.url === state.url && lastState.status === state.status) return; + lastState = state; + onChange?.(state); + }; + const reconcile = () => { + if (!active || disposed) return; + const target = nearestIndex(() => true); + if (target !== null && isReady(target)) displayedIndex = target; + else if (displayedIndex === null) displayedIndex = nearestIndex(isReady); + if (displayedIndex !== null) { + const asset = assets.get(sources[displayedIndex].url)!; + emit({ index: displayedIndex, url: asset.loadedUrl, image: asset.image, status: "ready" }); + } else { + const exhausted = [...assets.values()].every((asset) => asset.status === "failed"); + emit({ index: null, url: null, image: null, status: exhausted ? "failed" : "loading" }); + } + }; + + const request = (asset: Asset, run: number) => { + if (!isCurrent(run)) return; + asset.status = "loading"; + const image = createImage(); + asset.image = image; + image.loading = "eager"; + image.decoding = "async"; + const url = retryUrl(asset.url, asset.retries); + let settled = false; + let decoding = false; + const current = () => !settled && isCurrent(run) && asset.image === image; + const detach = () => { + image.removeEventListener("load", handleLoad); + image.removeEventListener("error", handleError); + asset.detach = null; + }; + const handleError = () => { + if (!current()) return; + settled = true; + detach(); + image.removeAttribute("src"); + asset.image = null; + if (asset.retries < RETRY_DELAYS.length) { + const delay = RETRY_DELAYS[asset.retries]; + asset.retries += 1; + asset.timer = setTimeout(() => { + if (!isCurrent(run)) return; + asset.timer = null; + request(asset, run); + }, delay); + } else { + asset.status = "failed"; + reconcile(); + } + }; + const handleLoad = () => { + if (!current() || decoding) return; + decoding = true; + void image.decode().then(() => { + if (!current()) return; + settled = true; + detach(); + asset.loadedUrl = url; + asset.status = "ready"; + reconcile(); + }, handleError); + }; + asset.detach = detach; + image.addEventListener("load", handleLoad); + image.addEventListener("error", handleError); + image.src = url; + }; + + const stop = () => { + active = false; + generation += 1; + for (const asset of assets.values()) { + if (asset.timer !== null) clearTimeout(asset.timer); + asset.timer = null; + asset.detach?.(); + asset.image?.removeAttribute("src"); + asset.image = null; + asset.status = "idle"; + asset.loadedUrl = null; + asset.retries = 0; + } + displayedIndex = null; + lastState = null; + }; + + return { + start() { + if (disposed || active) return; + active = true; + const run = ++generation; + reconcile(); + for (const asset of assets.values()) request(asset, run); + }, + select(level: number) { + if (disposed || !Number.isFinite(level)) return; + targetLevel = level; + reconcile(); + }, + stop, + dispose() { + stop(); + disposed = true; + onChange = null; + }, + }; +} diff --git a/src/client/skin.css b/src/client/skin.css index 2e22051..599ba76 100644 --- a/src/client/skin.css +++ b/src/client/skin.css @@ -1,6 +1,6 @@ .liang-skin-backdrop { position: fixed; - z-index: 0; + z-index: -1; inset: 0; overflow: hidden; pointer-events: none; @@ -19,7 +19,7 @@ body[data-liang-skin="on"] .liang-skin-backdrop { right: 0; display: block; width: min(42vw, 720px); - height: 100vh; + height: 100%; object-fit: cover; object-position: 50% 42%; opacity: var(--liang-portrait-opacity, 0.45); @@ -46,11 +46,16 @@ body[data-liang-skin="on"] .liang-skin-backdrop { position: absolute; inset: 0; content: ""; - background: linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%); + background: + linear-gradient(90deg, var(--liang-page, transparent) 0 28%, transparent 57% 100%), + linear-gradient(var(--liang-bg-base, transparent), var(--liang-bg-base, transparent)); } body[data-liang-skin="on"] { - --dsw-alias-bg-base: var(--liang-bg-base) !important; + /* Keep the backdrop above the canvas but below content and native portals. + The host owns root positioning, dimensions, and its own stacking order. */ + isolation: isolate; + --dsw-alias-bg-base: transparent !important; --dsw-alias-bg-layer-1: var(--liang-layer-1) !important; --dsw-alias-bg-layer-2: var(--liang-layer-2) !important; --dsw-alias-bg-layer-3: var(--liang-layer-3) !important; @@ -210,11 +215,16 @@ body[data-liang-skin="on"]:is( } body[data-liang-skin="on"] > #root { - position: relative; - z-index: 1; background: transparent !important; } +body[data-liang-skin="on"]:is( + [data-dsh-desktop-mode="compatibility"], + [data-dsh-desktop-mode="extended"] +) .liang-skin-backdrop { + top: var(--dsh-desktop-frame-height, 0px); +} + /* Windows paints the sidebar fill across the host frame, covering the portrait. Match macOS's transparent frame while keeping the sidebar, caption, and conversation surfaces painted by their own host rules. */ @@ -451,7 +461,7 @@ body[data-liang-skin="on"] .liang-effort-control__range::-webkit-slider-thumb { display: flex; } -[class*="_8HJdBW_cubeRow"]:has(.liang-appearance-choice) { +[class*="_cubeRow"]:has(.liang-appearance-choice) { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }