import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react"; import type { ClientContext, SnapshotStore } from "@deepseek-ai/dsh-client-runtime/client"; 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 { PREVIEW_MAX_FRAME, frameForEffort, indicatorLabel, nearestEffortIndex, paletteForFrame, portraitBlendForLevel, selectedEffortIndex, type EffortLike, } from "./logic"; const PACKAGE_ID = "dsh-client-liang-intensity-skin"; const LOCALE_NAMESPACE = "liang.skin"; const ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`; const BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort"; const PORTRAIT_ANCHORS = [ { level: 0, file: "stage-00.png" }, { level: 1, file: "level-01.png" }, { level: 3, file: "level-03.png" }, { level: 4, file: "level-04.png" }, { level: 6, file: "stage-06.png" }, { level: 7, file: "level-07.png" }, { level: 9, file: "level-09.png" }, { level: 10, file: "level-10.png" }, { level: 12, file: "stage-12.png" }, { level: 13, file: "level-13.png" }, { level: 14, file: "level-14.png" }, { level: 15, file: "bridge-15.png" }, { level: 16, file: "level-16.png" }, { level: 17, file: "level-17.png" }, { level: 18, file: "stage-18.png" }, { level: 19, file: "level-19.png" }, { level: 21, file: "level-21.png" }, { level: 22, file: "level-22.png" }, { level: 24, file: "stage-24.png" }, { level: 25, file: "level-25.png" }, { level: 27, file: "bridge-27.png" }, { level: 28, file: "level-28.png" }, { level: 29, file: "level-29.png" }, { level: 30, file: "stage-30.png" }, ] as const; const ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level); interface SkinSettings { enabled: boolean; bindEffort: boolean; } interface PreferenceStore { getSnapshot(): SkinSettings; subscribe(listener: () => void): () => void; set(enabled: boolean): Promise; setBindEffort(enabled: boolean): Promise; dispose(): void; } type NativeThemeId = "light" | "dark"; interface ThemeService { getTheme(): { preference: string }; setTheme(id: NativeThemeId | "system"): void; } interface ModelSelection { provider: string; model: string; reasoningEffort?: string; } interface CatalogModel { id: string; reasoning?: { efforts: EffortLike[]; defaultEffort?: string; }; } interface ModelDirectoryState { current: ModelSelection | null; groups: readonly { id: string; models: CatalogModel[] }[]; status: "idle" | "loading" | "ready" | "selecting" | "error"; error: string | null; } interface ModelDirectory { store: SnapshotStore; load(): Promise; select(selection: ModelSelection): Promise; } interface SliderProps { directory: SnapshotStore; load: () => void; select: (selection: ModelSelection) => Promise; presenter: SkinPresenter; scope: PreferenceStore; } interface SettingsRowProps { scope: PreferenceStore; presenter: SkinPresenter; theme: ThemeService; subscribeTheme: (listener: () => void) => () => void; t: (key: string) => string; } const cssVariables = [ "--liang-strength", "--liang-page", "--liang-bg-base", "--liang-layer-1", "--liang-layer-2", "--liang-layer-3", "--liang-sidebar", "--liang-ink", "--liang-secondary", "--liang-tertiary", "--liang-border", "--liang-accent", "--liang-accent-hover", "--liang-hover", "--liang-portrait-opacity", ] as const; class SkinPresenter { private readonly scope: PreferenceStore; private readonly theme: ThemeService; private readonly root: HTMLDivElement; private readonly poster: HTMLImageElement; private readonly portrait: HTMLImageElement; private readonly preloads: HTMLImageElement[]; private portraitReady = false; 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. private frame = PREVIEW_MAX_FRAME; private pendingFrame = PREVIEW_MAX_FRAME; private raf = 0; private disposed = false; private unsubscribe: () => void; constructor(scope: PreferenceStore, theme: ThemeService) { this.scope = scope; this.theme = theme; this.root = document.createElement("div"); this.root.className = "liang-skin-backdrop"; this.root.dataset.plugin = PACKAGE_ID; // Keep the stable poster visible while every portrait frame is fetched // and decoded. A request being complete does not mean the bitmap is ready // for a tear-free first swap. this.root.dataset.media = "poster"; this.root.setAttribute("aria-hidden", "true"); this.poster = document.createElement("img"); this.poster.className = "liang-skin-poster"; this.poster.src = `${ASSET_PREFIX}/liang-poster.png`; this.poster.alt = ""; 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.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.root.dataset.media = "sequence"; this.updatePortrait(paletteForFrame(this.frame).level); }, () => { if (!this.disposed) this.root.dataset.media = "poster"; }, ); this.root.append(this.portrait, this.poster); document.body.prepend(this.root); this.poster.addEventListener("error", this.handlePosterError); this.unsubscribe = scope.subscribe(() => this.syncSettings()); this.syncSettings(); } private readonly handleSequenceError = () => { this.root.dataset.media = "poster"; }; private readonly handlePosterError = () => { this.root.dataset.media = "color"; }; private syncSettings() { this.setEnabled(this.scope.getSnapshot().enabled); } isEnabled() { return this.enabled; } getFrame() { return this.frame; } setEnabled(enabled: boolean) { this.enabled = enabled; if (enabled) { if (!this.root.isConnected) document.body.prepend(this.root); document.body.dataset.liangSkin = "on"; this.applyFrame(); } else { this.root.remove(); delete document.body.dataset.liangSkin; delete document.body.dataset.liangStage; for (const name of cssVariables) document.body.style.removeProperty(name); } } setFrame(frame: number) { this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame))); this.frame = this.pendingFrame; if (!this.enabled) return; if (this.raf !== 0) return; this.raf = requestAnimationFrame(() => { this.raf = 0; this.frame = this.pendingFrame; this.applyFrame(); }); } private applyFrame() { const palette = paletteForFrame(this.frame); const body = document.body; this.syncNativeTheme(palette.stage === 5 ? "dark" : "light"); body.dataset.liangStage = String(palette.stage); body.style.setProperty("--liang-strength", String(palette.strength)); body.style.setProperty("--liang-page", palette.page); body.style.setProperty("--liang-bg-base", palette.base); body.style.setProperty("--liang-layer-1", palette.layer1); body.style.setProperty("--liang-layer-2", palette.layer2); body.style.setProperty("--liang-layer-3", palette.layer3); body.style.setProperty("--liang-sidebar", palette.sidebar); body.style.setProperty("--liang-ink", palette.ink); body.style.setProperty("--liang-secondary", palette.secondary); body.style.setProperty("--liang-tertiary", palette.tertiary); body.style.setProperty("--liang-border", palette.border); body.style.setProperty("--liang-accent", palette.accent); body.style.setProperty("--liang-accent-hover", palette.accentHover); body.style.setProperty("--liang-hover", palette.hover); body.style.setProperty("--liang-portrait-opacity", palette.portraitOpacity); this.updatePortrait(palette.level); } syncNativeTheme(theme: NativeThemeId = paletteForFrame(this.frame).stage === 5 ? "dark" : "light") { if (!this.enabled || this.theme.getTheme().preference === theme) return; this.theme.setTheme(theme); } 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; } async choose(enabled: boolean) { this.setEnabled(enabled); try { await this.scope.set(enabled); } catch (error) { this.syncSettings(); throw error; } } dispose() { if (this.disposed) return; this.disposed = true; this.unsubscribe(); if (this.raf !== 0) cancelAnimationFrame(this.raf); this.portrait.removeEventListener("error", this.handleSequenceError); this.poster.removeEventListener("error", this.handlePosterError); for (const image of this.preloads) image.src = ""; this.root.remove(); document.body.removeAttribute("data-liang-skin"); delete document.body.dataset.liangStage; for (const name of cssVariables) document.body.style.removeProperty(name); } } function modelReasoning(state: ModelDirectoryState) { const current = state.current; if (current === null) return null; const group = state.groups.find((item) => item.id === current.provider); const model = group?.models.find((item) => item.id === current.model); if (model?.reasoning === undefined) return null; return { selection: current, efforts: model.reasoning.efforts, defaultEffort: model.reasoning.defaultEffort, }; } function LiangEffortSlider({ directory, load, select, presenter, scope }: SliderProps) { const state = useSyncExternalStore( (listener) => directory.subscribe(listener), () => directory.getSnapshot(), ); const skin = useSyncExternalStore( (listener) => scope.subscribe(listener), () => scope.getSnapshot(), ); const reasoning = useMemo(() => modelReasoning(state), [state]); const efforts = reasoning?.efforts ?? []; const committedIndex = selectedEffortIndex( efforts, reasoning?.selection.reasoningEffort, reasoning?.defaultEffort, ); // An unknown committed effort defaults to the max frame: a fresh // conversation paints the dark shell immediately instead of flashing the // light palette until the directory load resolves the real effort. const committedFrame = committedIndex < 0 ? PREVIEW_MAX_FRAME : frameForEffort(committedIndex, efforts.length); const bindEffort = skin.bindEffort; const [frame, setFrame] = useState(() => bindEffort ? committedFrame : presenter.getFrame()); const [pending, setPending] = useState(false); const [failed, setFailed] = useState(false); const [interacting, setInteracting] = useState(false); const dragging = useRef(false); const dragStartFrame = useRef(frame); const enabled = skin.enabled; useEffect(() => { if (enabled) load(); }, [enabled, load]); useEffect(() => { if (!bindEffort || dragging.current || pending) return; setFrame(committedFrame); presenter.setFrame(committedFrame); }, [bindEffort, committedFrame, pending, presenter]); if (!enabled) return null; if (bindEffort && (reasoning === null || efforts.length < 2)) return null; const previewIndex = nearestEffortIndex(frame, efforts); const previewEffort = efforts[previewIndex]; const progressRatio = frame / PREVIEW_MAX_FRAME; const tooltipLabel = indicatorLabel(frame, bindEffort ? efforts : []); const commit = async (rawFrame: number) => { if (!bindEffort) { dragging.current = false; setFrame(rawFrame); presenter.setFrame(rawFrame); return; } const targetIndex = nearestEffortIndex(rawFrame, efforts); const target = efforts[targetIndex]; if (target === undefined) return; const targetFrame = frameForEffort(targetIndex, efforts.length); dragging.current = false; setFrame(targetFrame); presenter.setFrame(targetFrame); if (targetIndex === committedIndex || pending) return; setPending(true); setFailed(false); const accepted = await select({ provider: reasoning.selection.provider, model: reasoning.selection.model, reasoningEffort: target.id, }); setPending(false); if (!accepted) { setFailed(true); setFrame(committedFrame); presenter.setFrame(committedFrame); } }; return (
{interacting && ( {tooltipLabel} )} { dragging.current = true; dragStartFrame.current = frame; setInteracting(true); }} onInput={(event) => { dragging.current = true; const next = Number(event.currentTarget.value); setFrame(next); presenter.setFrame(next); }} onPointerUp={(event) => { setInteracting(false); void commit(Number(event.currentTarget.value)); }} onPointerCancel={() => { setInteracting(false); dragging.current = false; setFrame(dragStartFrame.current); presenter.setFrame(dragStartFrame.current); }} onKeyUp={(event) => { setInteracting(false); if (event.key !== "Escape") void commit(Number(event.currentTarget.value)); }} onBlur={(event) => { setInteracting(false); if (dragging.current) void commit(Number(event.currentTarget.value)); }} onKeyDown={(event) => { setInteracting(true); if (event.key === "Escape" && !pending) { setInteracting(false); dragging.current = false; setFrame(dragStartFrame.current); presenter.setFrame(dragStartFrame.current); } }} />
); } type AppearanceChoice = NativeThemeId | "system" | "liang"; const APPEARANCE_CHOICES: readonly { id: AppearanceChoice; labelKey: string }[] = [ { id: "light", labelKey: "appearance.light" }, { id: "dark", labelKey: "appearance.dark" }, { id: "system", labelKey: "appearance.system" }, { id: "liang", labelKey: "appearance.liang" }, ]; function NativeAppearanceIcon({ id }: { id: Exclude }) { if (id === "light") { return ( ); } if (id === "dark") { return ( ); } return ( ); } function AppearanceSkinRow({ scope, presenter, theme, subscribeTheme, t }: SettingsRowProps) { const snapshot = useSyncExternalStore( (listener) => scope.subscribe(listener), () => scope.getSnapshot(), ); const preference = useSyncExternalStore( subscribeTheme, () => theme.getTheme().preference, ); const selected = snapshot.enabled ? "liang" : preference === "light" || preference === "dark" || preference === "system" ? preference : "system"; const [pending, setPending] = useState(false); const choose = async (next: AppearanceChoice) => { if (next === selected || pending) return; setPending(true); try { if (next === "liang") { await presenter.choose(true); } else { // Disable the custom skin first. This is intentionally unconditional: // it makes the native choice the only writer after this click, even if // the external preference store is one render behind. await presenter.choose(false); theme.setTheme(next); } } finally { setPending(false); } }; return (
{t("appearance.title")}
{APPEARANCE_CHOICES.map(({ id, labelKey }) => ( ))}
); } 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", "modelDirectories", "locale", "theme", ]; function createPreferenceStore(): PreferenceStore { // The market's active skin is the source of truth for whether this client // should be visible. The appearance switch is therefore scoped to this // client activation and must not survive switching away and back. try { localStorage.removeItem("dsh-liang-intensity-skin.enabled"); } catch { // Storage may be unavailable; the in-memory default still enables Liang. } let snapshot: SkinSettings = { enabled: true, bindEffort: localStorage.getItem(BIND_EFFORT_KEY) !== "0", }; const listeners = new Set<() => void>(); const onStorage = (event: StorageEvent) => { if (event.key !== BIND_EFFORT_KEY) return; const next = { enabled: snapshot.enabled, bindEffort: event.newValue !== "0", }; if (next.enabled === snapshot.enabled && next.bindEffort === snapshot.bindEffort) return; snapshot = next; for (const listener of listeners) listener(); }; window.addEventListener("storage", onStorage); return { getSnapshot: () => snapshot, subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); }, async set(enabled) { if (enabled === snapshot.enabled) return; snapshot = { ...snapshot, enabled }; for (const listener of listeners) listener(); }, async setBindEffort(bindEffort) { if (bindEffort === snapshot.bindEffort) return; localStorage.setItem(BIND_EFFORT_KEY, bindEffort ? "1" : "0"); snapshot = { ...snapshot, bindEffort }; for (const listener of listeners) listener(); }, dispose() { window.removeEventListener("storage", onStorage); listeners.clear(); }, }; } export function apply(ctx: ClientContext) { const style = document.createElement("style"); style.dataset.plugin = PACKAGE_ID; style.textContent = styles; document.head.append(style); ctx.effect(() => () => style.remove(), "liang-intensity-skin: scoped styles"); const scope = createPreferenceStore(); ctx.effect(() => () => scope.dispose(), "liang-intensity-skin: appearance preference"); const theme = ctx.get("theme") as ThemeService; const presenter = new SkinPresenter(scope, theme); ctx.effect(() => () => presenter.dispose(), "liang-intensity-skin: backdrop presenter"); ctx.effect( () => installLiangAppearanceButton(scope, presenter), "liang-intensity-skin: native appearance extension", ); ctx.effect( () => ctx.locale.register(LOCALE_NAMESPACE, { zh: { "appearance.title": "外观", "appearance.light": "浅色", "appearance.dark": "深色", "appearance.system": "跟随系统", "appearance.liang": "滑动变祖", }, en: { "appearance.title": "Appearance", "appearance.light": "Light", "appearance.dark": "Dark", "appearance.system": "System", "appearance.liang": "Slider", }, }), "liang-intensity-skin: settings locale"); ctx.slots.inject("conversation.input.right", () => ctx.slots.register({ name: "conversation.input.right", id: "liang-intensity-control", order: 10, inject: (sessionId: string) => { const available = ctx.sessions.subagentAddress(sessionId) === undefined; const directory = ctx.modelDirectories.directoryFor(sessionId) as ModelDirectory; return { directory: directory.store, presenter, scope, load: () => { if (available) void directory.load().catch(() => undefined); }, select: (selection: ModelSelection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false), }; }, }, LiangEffortSlider)); }