Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.6.4 — 2026-09-13

### 修复

- Windows 版恢复「原生 / QQ / 自定义」皮肤模式切换,不再直接提示该功能仅支持 macOS。
- `data-reduced-motion="true"` 现在会真正停用 QQ 雨丝、雷电动画和自定义主题动态层,避免持续合成与重绘。
- MutationObserver 忽略皮肤自身产生的 DOM 变更,减少观察器反复触发完整注入流程带来的卡顿。
- Windows 皮肤库切换改用脱离 Codex 进程树的 WMI 启动方式,避免切到一半时 Codex 被连带退出。

## 2.6.3 — 2026-07-26

- 修复霓虹雨夜主题下右下角用户菜单白底浅字、几乎不可读的问题;菜单强制深色背景与浅色文字。
Expand Down
16 changes: 16 additions & 0 deletions assets/custom-skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,17 @@ html.codex-dream-skin [data-message-author-role] { border-radius: 16px; }
}
}

/* Codex 26.9 exposes reduced motion as a root data attribute rather than an OS
media query. Honor either signal so Neon Storm and room animations stop. */
html.codex-dream-skin[data-reduced-motion="true"] *,
html.codex-dream-skin[data-reduced-motion="true"] *::before,
html.codex-dream-skin[data-reduced-motion="true"] *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}

/* Full-window custom wallpaper. Paint the uploaded image once behind the UI,
then soften it so chat text stays readable (same idea as the frosted sidebar). */
html.codex-dream-skin body {
Expand Down Expand Up @@ -1799,3 +1810,8 @@ html.codex-dream-skin[data-qq-settings="true"] main.main-surface [class*="text-t
opacity: 0.28;
}
}

html.codex-dream-skin[data-reduced-motion="true"][data-qq-weather="neon-storm"]
#codex-qq-skin-weather {
opacity: 0.28;
}
11 changes: 11 additions & 0 deletions assets/qq-skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -3878,6 +3878,17 @@ html.codex-qq-skin[data-dream-shell="light"]
}
}

/* Codex 26.9 exposes reduced motion as a root data attribute rather than an OS
media query. Honor either signal so rain and room animations stop reliably. */
html.codex-qq-skin[data-reduced-motion="true"] *,
html.codex-qq-skin[data-reduced-motion="true"] *::before,
html.codex-qq-skin[data-reduced-motion="true"] *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}

/* Windows/settings corrections. The settings screen owns a separate navigation
panel, so task-sidebar profile decoration must not leak into it. */
html.codex-qq-skin:is(
Expand Down
18 changes: 15 additions & 3 deletions assets/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -1802,9 +1802,11 @@

try {
mq = window.matchMedia("(prefers-reduced-motion: reduce)");
reduced = Boolean(mq.matches);
const reducedMotionEnabled = () => Boolean(mq.matches) ||
document.documentElement?.getAttribute("data-reduced-motion") === "true";
reduced = reducedMotionEnabled();
mq._handler = () => {
reduced = Boolean(mq.matches);
reduced = reducedMotionEnabled();
if (reduced) {
stop();
paint();
Expand Down Expand Up @@ -3684,7 +3686,17 @@
scheduler.timeout = setTimeout(flushScheduledEnsure, 64);
}
};
const observer = new MutationObserver(() => scheduleEnsure({ route: true }));
const skinOwnedSelector = '[id^="codex-qq-skin-"], [class*="qq-skin-"], [class*="dream-skin-"], [class*="dream-retro-"]';
const nodeIsSkinOwned = (node) => node?.nodeType === 1 &&
(node.matches?.(skinOwnedSelector) || Boolean(node.closest?.(skinOwnedSelector)));
const observer = new MutationObserver((records) => {
const hasNativeMutation = records.some((record) => {
if (record.target?.closest?.(skinOwnedSelector)) return false;
const changed = [...record.addedNodes, ...record.removedNodes];
return changed.length === 0 || changed.some((node) => !nodeIsSkinOwned(node));
});
if (!records.length || hasNativeMutation) scheduleEnsure({ route: true });
});
rootObserver = new MutationObserver(() => {
if (samplingNativeShell) return;
scheduleEnsure({ root: true, route: true });
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-qq-skin",
"version": "2.6.3",
"version": "2.6.4",
"private": true,
"type": "module",
"scripts": {
Expand Down
33 changes: 29 additions & 4 deletions scripts/injector.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -818,13 +818,34 @@ function listThemeLibrary(themeDir, { limit = 12, customOnly = false } = {}) {
return items.slice(0, Math.max(1, limit)).map(({ id, name, kind, active }) => ({ id, name, kind, active }));
}

function runWindowsLibrarySwitch(themeId) {
const script = path.join(root, "scripts", "windows", "switch-theme-windows.ps1");
const commandLine = ["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", `"${script}"`, "-Id", `"${themeId}"`].join(" ");
const launcher = [
`$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = '${commandLine.replaceAll("'", "''")}'; CurrentDirectory = '${root.replaceAll("'", "''")}' }`,
`if ($result.ReturnValue -ne 0) { throw "Win32_Process.Create failed: $($result.ReturnValue)" }`,
].join("; ");
return new Promise((resolve, reject) => {
// WMI starts the switcher outside Codex's process tree. A direct child is
// killed together with this injector during a full Windows restart.
const child = spawn("powershell.exe", [
"-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", launcher,
], { env: process.env, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
let stderr = "";
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) resolve();
else reject(new Error(stderr.trim() || `Windows library switch launcher exited with ${code}`));
});
});
}

function runLibrarySwitch(themeId) {
if (!/^[A-Za-z0-9_-]{1,80}$/.test(themeId || "")) {
return Promise.reject(new Error(`Invalid theme id: ${themeId}`));
}
if (process.platform === "win32") {
return Promise.reject(new Error("In-app library switching is currently macOS-only"));
}
if (process.platform === "win32") return runWindowsLibrarySwitch(themeId);
const script = path.join(root, "scripts", "switch-theme-macos.sh");
return new Promise((resolve, reject) => {
// --no-apply only stages the live theme pack. The watch loop refreshes
Expand Down Expand Up @@ -1388,7 +1409,7 @@ async function runWatch(options) {
};

const pollLibrarySwitchRequests = async () => {
if (librarySwitchBusy || process.platform === "win32" || !sessions.size) return;
if (librarySwitchBusy || !sessions.size) return;
for (const record of sessions.values()) {
if (record.session.closed) continue;
let themeId = null;
Expand All @@ -1411,6 +1432,10 @@ async function runWatch(options) {
librarySwitchBusy = true;
try {
await runLibrarySwitch(themeId);
if (process.platform === "win32") {
console.log(`[qq-skin] handed library theme ${themeId} to detached Windows switcher`);
return;
}
await record.session.evaluate(`try {
window.localStorage?.setItem("codex-qq-skin-mode", "custom");
window.localStorage?.setItem("codex-qq-skin-enabled", "true");
Expand Down
8 changes: 8 additions & 0 deletions tests/renderer-inject.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,14 @@ assert.match(template, /const TOGGLE_ID = "codex-qq-skin-toggle"[\s\S]{0,400}cod
"The renderer must ship a persistent three-mode UI selector.");
assert.match(template, /\["native", "原生"\][\s\S]{0,80}\["qq", "QQ"\][\s\S]{0,80}\["custom", "自定义"\]/,
"The title-bar selector must expose native, stable QQ, and custom skins.");
assert.match(template, /getAttribute\("data-reduced-motion"\) === "true"/,
"Renderer motion must honor Codex's root reduced-motion attribute.");
assert.match(css, /html\.codex-qq-skin\[data-reduced-motion="true"\]/,
"QQ CSS must honor the root reduced-motion attribute.");
assert.match(customCss, /html\.codex-dream-skin\[data-reduced-motion="true"\]/,
"Custom CSS must honor the root reduced-motion attribute.");
assert.match(template, /nodeIsSkinOwned[\s\S]{0,500}hasNativeMutation[\s\S]{0,120}scheduleEnsure/,
"The renderer observer must ignore its own skin-owned DOM mutations.");
assert.match(template, /LIBRARY_SWITCH_KEY = "codex-qq-skin-library-switch"/,
"Codex must request library switches through a dedicated localStorage channel.");
assert.match(template, /dataset\.skinLibrary = "recent"/,
Expand Down
8 changes: 8 additions & 0 deletions tests/windows/run-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ if ($commonWindows | Where-Object { $_ -match '\(Get-CodexProcesses\)\.Count' -a
throw 'Get-CodexProcesses count checks must use @(...) under StrictMode.'
}

$injectorSource = Get-Content -LiteralPath (Join-Path $root 'scripts\injector.mjs') -Raw
if ($injectorSource -match 'In-app library switching is currently macOS-only') {
throw 'Windows in-app library switching regressed to macOS-only.'
}
if ($injectorSource -notmatch 'Invoke-CimMethod -ClassName Win32_Process') {
throw 'Windows library switching must launch outside the injector process tree.'
}

& $node (Join-Path $root 'scripts\injector.mjs') --check-payload --theme-dir (Join-Path $root 'presets\preset-classic-codex') | Out-Null
if ($LASTEXITCODE -ne 0) { throw 'Injector payload check failed.' }
$bumblebee = Join-Path $root 'presets\preset-bumblebee'
Expand Down