From efa4d42232f644f7babf546f089051296437e9f7 Mon Sep 17 00:00:00 2001 From: xiaojie0815 Date: Tue, 10 Nov 2026 00:26:14 +0800 Subject: [PATCH 1/5] fix(theme): align Windows shell and reduce chat-switch style invalidation Use semantic theme parts to reset Windows sidebar/header gutters across the workspace sidebar and detached page header layouts. Cache the two remaining Dream Skin composer predicates through the existing marker lifecycle, retaining specificity and observing all inputs. Install the manager popup parent rule only while its page is open. Add stylesheet/lifecycle regressions and real Chromium coverage for conditional paint, mutation updates, alignment and cleanup. Validation: 197 related Node tests and Chromium regression matrix pass. --- .../src/renderer-inject.test.ts | 2 +- .../src/theme-shell-performance.test.ts | 78 +++++++++ .../inject/dream-skin-native-shell-compat.css | 12 ++ assets/inject/renderer-inject.js | 33 +++- assets/inject/renderer-inject/00-prelude.js | 2 +- assets/inject/renderer-inject/10-style.js | 27 ++- .../renderer-inject/40-backend-settings.js | 4 +- assets/inject/renderer-inject/manifest.json | 4 +- .../dream-skin/macos/renderer-inject.js | 26 ++- .../dream-skin/windows/renderer-inject.js | 26 ++- scripts/test-theme-shell-performance.cjs | 164 ++++++++++++++++++ 11 files changed, 353 insertions(+), 25 deletions(-) create mode 100644 apps/codex-plus-manager/src/theme-shell-performance.test.ts create mode 100644 scripts/test-theme-shell-performance.cjs diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index 851302f285..d5c8f0b67c 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -4537,7 +4537,7 @@ describe("功能页与原生弹出层的层级", () => { it("页面打开时连同弹出层的直接父容器一起提升,原生 tooltip 不被页面盖住", async () => { const style = await readFile(new URL("../../../assets/inject/renderer-inject/10-style.js", import.meta.url), "utf8"); // 新版 Codex 把 tooltip 挂在 z-60 的固定视口容器里,只提升子节点逃不出该堆叠上下文(实测复现过)。 - assert.match(style, /html\[data-codex-plus-page-open\] div:has\(> \[data-radix-popper-content-wrapper\], > \[role="tooltip"\], > \[role="menu"\]\):not\(\.\$\{codexPlusPageClass\} \*\),/); + assert.match(style, /html\[data-codex-plus-page-open\] div:has\(> \[data-radix-popper-content-wrapper\], > \[role="tooltip"\], > \[role="menu"\]\):not\(\.\$\{codexPlusPageClass\} \*\) \{/); const page = Number(/z-index: (\d+);\s*display: block;\s*background: var\(--codex-plus-bg-primary/.exec(style)?.[1]); const lifted = Number(/\.app-header-tint \{\s*z-index: (\d+) !important;/.exec(style)?.[1]); assert.ok(page > 0 && lifted > page, "提升后的弹出层必须高于功能页"); diff --git a/apps/codex-plus-manager/src/theme-shell-performance.test.ts b/apps/codex-plus-manager/src/theme-shell-performance.test.ts new file mode 100644 index 0000000000..de7f7f50db --- /dev/null +++ b/apps/codex-plus-manager/src/theme-shell-performance.test.ts @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { it } from "node:test"; + +const asset = (name: string) => readFile(new URL(`../../../assets/inject/${name}`, import.meta.url), "utf8"); + +for (const platform of ["windows", "macos"]) { + it(`${platform}: removes stylesheet :has() dependencies from base and community themes`, async () => { + const renderer = await asset(`upstream/dream-skin/${platform}/renderer-inject.js`); + const css = [ + await asset(`upstream/dream-skin/${platform}/dream-skin.css`), + await asset("dream-skin-native-shell-compat.css"), + await asset("dream-skin-community-compat.css"), + ].join("\n"); + const start = renderer.indexOf(" const CSS_PREDICATES ="); + const end = renderer.indexOf(" const ART =", start); + assert.ok(start >= 0 && end > start); + const compile = new Function("cssText", `${renderer.slice(start, end)}\nreturn cssText;`); + const compiled = compile(css); + // 不仅检查新规则:防止添加样式后遗漏预计算表,重新引入常驻依赖。 + assert.doesNotMatch(compiled.replace(/\/\*[\s\S]*?\*\//g, ""), /:has\(/); + }); +} + +it("keeps the popup parent rule installed only while the manager page is open", async () => { + const styleSource = await asset("renderer-inject/10-style.js"); + const pageSource = await asset("renderer-inject/40-backend-settings.js"); + const extract = (source: string, name: string) => { + const match = source.match(new RegExp(`^ function ${name}\\([^]*?^ \\}`, "m")); + assert.ok(match, name); + return match[0]; + }; + assert.doesNotMatch(extract(styleSource, "installStyle"), /div:has\(/, + "manager popup parent rule must not remain in the always-installed sheet"); + let open = false; + let writes = 0; + type StyleNode = { id: string; textContent: string; setAttribute(): void; remove(): void }; + const nodes = new Map(); + const attributes = new Map(); + const document = { + getElementById: (id: string) => nodes.get(id), + querySelector: (selector: string) => selector === ".codex-plus-page-overlay" && open ? {} : null, + createElement: (): StyleNode => ({ + id: "", textContent: "", setAttribute() {}, + remove() { nodes.delete(this.id); }, + }), + documentElement: { + appendChild(node: StyleNode) { nodes.set(node.id, node); writes++; }, + setAttribute: (name: string, value: string) => attributes.set(name, value), + removeAttribute: (name: string) => attributes.delete(name), + style: { setProperty() {}, removeProperty() {} }, + }, + }; + const sync = new Function("document", ` + const styleId = "codex-delete-style"; + const codexPlusPageClass = "codex-plus-page-overlay"; + const codexPlusRailSelector = "nav[data-app-navigation-rail]"; + const codexPlusRailDestinationSelector = "[data-sidebar-destination]"; + ${extract(styleSource, "syncCodexPlusPagePopupStyle")} + ${extract(pageSource, "syncCodexPlusRailNativeSelection")} + return syncCodexPlusRailNativeSelection; + `)(document); + sync(); + assert.equal(nodes.size, 0); + open = true; + sync(); + sync(); + assert.equal(writes, 1, "repeated scans must not recreate the stylesheet"); + assert.equal(attributes.has("data-codex-plus-page-open"), true); + assert.match(nodes.get("codex-delete-style-page-popups")!.textContent, /div:has\(/); + open = false; + sync(); + assert.equal(nodes.size, 0, "closing removes the CSS rule, not just its root condition"); + assert.equal(attributes.has("data-codex-plus-page-open"), false); + open = true; + sync(); + assert.equal(nodes.size, 1, "reopening restores popup stacking"); +}); diff --git a/assets/inject/dream-skin-native-shell-compat.css b/assets/inject/dream-skin-native-shell-compat.css index 482312752b..ccd2e86089 100644 --- a/assets/inject/dream-skin-native-shell-compat.css +++ b/assets/inject/dream-skin-native-shell-compat.css @@ -6,4 +6,16 @@ :is([data-app-shell-page-surface="true"], [class*="_ApplicationMenuPageSurfaceLayout_"], [class~="bg-token-main-surface-primary"][class~="h-full"][class~="w-full"]):not([data-codex-composer-root], [data-codex-composer-root] *, [role="dialog"], [role="dialog"] *, [role="menu"], [role="menu"] *, [data-slot="popover-content"], [data-slot="popover-content"] *) { background: transparent !important; } + + /* 26.1103 的侧栏位于 workspace 内,header 则在 main 外。 + 使用公开部件标记适配两种层级,消除原生 8px/4px 留白造成的皮肤断层。 */ + html[data-dream-skin="active"][data-codex-os="win32"][data-codex-window-chrome="application-menu"] + [data-ds-part="sidebar"] { + margin-top: 0 !important; + } + + html[data-dream-skin="active"][data-codex-os="win32"][data-codex-window-chrome="application-menu"] + [data-ds-part="header"] { + margin-inline-end: 0 !important; + } } diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 1f75a36b33..c27d7ceae6 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -396,7 +396,7 @@ const styleId = "codex-delete-style"; // 改 10-style.js 里的任何 CSS 都要把它 +1:installStyle 靠这个版本号判断 // 页面里已有的 +
+
+
+
+
+
+
+ +
+
+
`); + await page.evaluate(runtime); + await page.evaluate(() => { + Object.assign(document.documentElement.dataset, {codexOs: "win32", dreamArtWide: "true", dreamTaskMode: "full"}); + }); + const marked = (id, marker, expected) => page.waitForFunction( + ({id, marker, expected}) => document.getElementById(id).hasAttribute(marker) === expected, + {id, marker, expected}); + await marked("home", homeMarker, true); + await marked("footer", timelineMarker, true); + assert.equal(await page.$eval("#decoy", n => n.hasAttribute("data-dream-has-timeline-composer")), false); + assert.equal(await page.$eval("#home-chrome", n => getComputedStyle(n).borderTopLeftRadius), "22px"); + assert.equal(await page.$eval("#backdrop", n => getComputedStyle(n).backgroundColor), "rgba(0, 0, 0, 0)"); + const nativeHas = await page.evaluate(() => { + const selectors = []; + const walk = rules => { for (const rule of rules) { + if (rule.selectorText?.includes(":has(")) selectors.push(rule.selectorText); + if (rule.cssRules) walk(rule.cssRules); + }}; + walk(window.__CODEX_DREAM_SKIN_STATE__.styleSheet.cssRules); + return selectors; + }); + assert.deepEqual(nativeHas, [], `${platform}: no persistent stylesheet :has()`); + + // 条件属性、归属属性、动态类名变化均须由真实 observer 刷新。 + await page.$eval("#utility", n => n.setAttribute("data-composer-home-utility-bar-position", "below")); + await marked("home", homeMarker, false); + await page.$eval("#utility", n => n.setAttribute("data-composer-home-utility-bar-position", "above")); + await marked("home", homeMarker, true); + await page.$eval("#home", n => n.setAttribute("data-composer-placement", "thread")); + await marked("home", homeMarker, false); + await page.$eval("#home", n => n.setAttribute("data-composer-placement", "home")); + await marked("home", homeMarker, true); + await page.$eval("#thread-chrome", n => n.className = "unrelated"); + await marked("footer", timelineMarker, false); + assert.equal(await page.$eval("#backdrop", n => getComputedStyle(n).backgroundColor), "rgb(12, 34, 56)"); + await page.$eval("#thread-chrome", n => n.className = "test_ComposerLayoutRoot_123"); + await marked("footer", timelineMarker, true); + await page.$eval("#footer", n => n.classList.remove("absolute")); + await marked("footer", timelineMarker, false); + await page.$eval("#footer", n => n.classList.add("absolute")); + await marked("footer", timelineMarker, true); + await page.$eval("#timeline", n => n.removeAttribute("data-app-action-timeline-scroll")); + await marked("footer", timelineMarker, false); + await page.$eval("#timeline", n => n.setAttribute("data-app-action-timeline-scroll", "")); + await marked("footer", timelineMarker, true); + + // detached 路由也应清理;重新注入后不得留下旧属性/定时器。 + await page.$eval("#footer", n => { window.detachedFooter = n; n.remove(); }); + await page.waitForFunction(() => !window.detachedFooter.hasAttribute("data-dream-has-timeline-composer")); + await page.evaluate(runtime); + await page.evaluate(() => window.__CODEX_DREAM_SKIN_STATE__.cleanup()); + assert.equal(await page.locator(`[${homeMarker}], [${timelineMarker}]`).count(), 0); + assert.equal(await page.evaluate(() => document.adoptedStyleSheets.length), 0); + console.log(`${platform}: stylesheet, mutation and cleanup regressions passed`); + } + + // 新版 header 在 main 外,aside 在 workspace 内;同时保留旧版结构。 + for (const structure of ["legacy", "workspace"]) { + await page.setContent(`${structure === "workspace" + ? '
' + : '
'}`); + await page.addStyleTag({content: asset("dream-skin-native-shell-compat.css")}); + const margins = () => page.evaluate(() => [getComputedStyle(document.getElementById("sidebar")).marginTop, + getComputedStyle(document.getElementById("header")).marginInlineEnd]); + const state = (os, active) => page.evaluate(({os,active}) => { + Object.assign(document.documentElement.dataset, {codexOs:os, codexWindowChrome:"application-menu"}); + if (active) document.documentElement.dataset.dreamSkin = "active"; + else delete document.documentElement.dataset.dreamSkin; + }, {os, active}); + await state("win32", true); + assert.deepEqual(await margins(), ["0px", "0px"]); + assert.deepEqual(await page.$eval("#header", n => [getComputedStyle(n).backgroundColor, getComputedStyle(n).backdropFilter]), ["rgb(11, 22, 33)", "blur(12px)"]); + await state("darwin", true); + assert.deepEqual(await margins(), ["8px", "4px"]); + await state("win32", false); + assert.deepEqual(await margins(), ["8px", "4px"]); + } + console.log("Windows alignment: old/new structures, disabled theme and macOS passed"); + + const styles = asset("renderer-inject/10-style.js"); + const pages = asset("renderer-inject/40-backend-settings.js"); + await page.setContent('
'); + await page.evaluate(`(() => { + const styleId = "codex-delete-style"; + const codexPlusPageClass = "codex-plus-page-overlay"; + const codexPlusRailSelector = "nav[data-app-navigation-rail]"; + const codexPlusRailDestinationSelector = "[data-sidebar-destination]"; + ${extract(styles, "syncCodexPlusPagePopupStyle")} + ${extract(pages, "syncCodexPlusRailNativeSelection")} + window.syncPage = syncCodexPlusRailNativeSelection; + window.syncPage(); + })()`); + assert.equal(await page.$eval("#outside", n => getComputedStyle(n).zIndex), "2147483645"); + assert.equal(await page.$eval("#inside", n => getComputedStyle(n).zIndex), "auto"); + await page.evaluate(() => { window.syncPage(); window.syncPage(); }); + assert.equal(await page.locator("#codex-delete-style-page-popups").count(), 1); + await page.locator(".codex-plus-page-overlay").evaluate(n => n.remove()); + await page.evaluate(() => window.syncPage()); + assert.equal(await page.locator("#codex-delete-style-page-popups").count(), 0); + assert.equal(await page.$eval("#outside", n => getComputedStyle(n).zIndex), "auto"); + console.log("Manager popups: parent stacking, internal exclusion and close cleanup passed"); + assert.deepEqual(errors, []); + } finally { + await browser.close(); + } +} +run().catch(error => { console.error(error); process.exitCode = 1; }); From 8fc6e9b92311ce4f8f2c7b095e23b3a18bd08b9d Mon Sep 17 00:00:00 2001 From: xiaojie0815 Date: Tue, 10 Nov 2026 00:27:41 +0800 Subject: [PATCH 2/5] test(theme): run shell browser regressions in Windows CI --- .github/workflows/pr-build.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 1459e88161..317dbb0398 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -84,6 +84,26 @@ jobs: working-directory: apps/codex-plus-manager run: npm test + - name: Install theme browser-test dependencies + shell: pwsh + run: | + # 与 image-overlay 回归一致,隔离测试依赖并使用 runner 自带 Edge。 + npm install --prefix "$env:RUNNER_TEMP/theme-shell-tests" --no-save --package-lock=false playwright@1.62.1 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + "NODE_PATH=$env:RUNNER_TEMP/theme-shell-tests/node_modules" >> $env:GITHUB_ENV + $browser = @( + "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", + "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" + ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 + if (-not $browser) { throw "找不到主题回归测试所需的 Edge 浏览器。" } + "CODEX_TEST_BROWSER_PATH=$browser" >> $env:GITHUB_ENV + + - name: Theme shell browser regressions + run: | + node scripts/assemble-renderer-inject.mjs --check + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node scripts/test-theme-shell-performance.cjs + - name: Native Fast regression tests run: >- node --test From fbc38689d548c4c02a13012012667a2c194e2dae Mon Sep 17 00:00:00 2001 From: xiaojie0815 Date: Tue, 10 Nov 2026 00:36:18 +0800 Subject: [PATCH 3/5] fix(theme): limit changes to tested Windows behavior --- .github/workflows/pr-build.yml | 20 -------------- .../src/theme-shell-performance.test.ts | 24 ++++++++++++++--- assets/inject/renderer-inject.js | 9 +++++-- assets/inject/renderer-inject/10-style.js | 9 +++++-- assets/inject/renderer-inject/manifest.json | 2 +- .../dream-skin/macos/renderer-inject.js | 26 +++---------------- scripts/test-theme-shell-performance.cjs | 5 ++-- 7 files changed, 42 insertions(+), 53 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 317dbb0398..1459e88161 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -84,26 +84,6 @@ jobs: working-directory: apps/codex-plus-manager run: npm test - - name: Install theme browser-test dependencies - shell: pwsh - run: | - # 与 image-overlay 回归一致,隔离测试依赖并使用 runner 自带 Edge。 - npm install --prefix "$env:RUNNER_TEMP/theme-shell-tests" --no-save --package-lock=false playwright@1.62.1 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - "NODE_PATH=$env:RUNNER_TEMP/theme-shell-tests/node_modules" >> $env:GITHUB_ENV - $browser = @( - "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", - "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" - ) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 - if (-not $browser) { throw "找不到主题回归测试所需的 Edge 浏览器。" } - "CODEX_TEST_BROWSER_PATH=$browser" >> $env:GITHUB_ENV - - - name: Theme shell browser regressions - run: | - node scripts/assemble-renderer-inject.mjs --check - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - node scripts/test-theme-shell-performance.cjs - - name: Native Fast regression tests run: >- node --test diff --git a/apps/codex-plus-manager/src/theme-shell-performance.test.ts b/apps/codex-plus-manager/src/theme-shell-performance.test.ts index de7f7f50db..b4eb5e2413 100644 --- a/apps/codex-plus-manager/src/theme-shell-performance.test.ts +++ b/apps/codex-plus-manager/src/theme-shell-performance.test.ts @@ -4,7 +4,7 @@ import { it } from "node:test"; const asset = (name: string) => readFile(new URL(`../../../assets/inject/${name}`, import.meta.url), "utf8"); -for (const platform of ["windows", "macos"]) { +for (const platform of ["windows"]) { it(`${platform}: removes stylesheet :has() dependencies from base and community themes`, async () => { const renderer = await asset(`upstream/dream-skin/${platform}/renderer-inject.js`); const css = [ @@ -30,8 +30,6 @@ it("keeps the popup parent rule installed only while the manager page is open", assert.ok(match, name); return match[0]; }; - assert.doesNotMatch(extract(styleSource, "installStyle"), /div:has\(/, - "manager popup parent rule must not remain in the always-installed sheet"); let open = false; let writes = 0; type StyleNode = { id: string; textContent: string; setAttribute(): void; remove(): void }; @@ -53,6 +51,7 @@ it("keeps the popup parent rule installed only while the manager page is open", }; const sync = new Function("document", ` const styleId = "codex-delete-style"; + const codexPlusIsWindowsPlatform = true; const codexPlusPageClass = "codex-plus-page-overlay"; const codexPlusRailSelector = "nav[data-app-navigation-rail]"; const codexPlusRailDestinationSelector = "[data-sidebar-destination]"; @@ -76,3 +75,22 @@ it("keeps the popup parent rule installed only while the manager page is open", sync(); assert.equal(nodes.size, 1, "reopening restores popup stacking"); }); + +it("keeps the original popup stylesheet behavior on non-Windows platforms", async () => { + const source = await asset("renderer-inject/10-style.js"); + const install = source.match(/^ function installStyle\([^]*?^ \}/m)![0]; + const names = [...new Set(["styleId", "codexDeleteStyleVersion", + ...Array.from(install.matchAll(/\$\{([A-Za-z_$][A-Za-z0-9_$]*)/g), m => m[1])])]; + for (const windows of [false, true]) { + let text = ""; + const document = { + getElementById: () => null, + createElement: () => ({id: "", dataset: {}, textContent: ""}), + documentElement: {appendChild: (style: {textContent: string}) => {text = style.textContent;}}, + }; + new Function("document", ...names, `${install}\ninstallStyle();`)(document, + ...names.map(name => name === "codexPlusIsWindowsPlatform" ? windows : name === "selectors" ? {} : name)); + assert.equal(text.includes("div:has("), !windows, + "only Windows should stop installing the persistent parent rule"); + } +}); diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index c27d7ceae6..655209dd77 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -1187,6 +1187,11 @@ letter-spacing: -0.15px; white-space: nowrap; } + ${codexPlusIsWindowsPlatform ? "" : ` + html[data-codex-plus-page-open] div:has(> [data-radix-popper-content-wrapper], > [role="tooltip"], > [role="menu"]):not(.${codexPlusPageClass} *) { + z-index: 2147483645 !important; + } + `} html[data-codex-plus-page-open] [data-radix-popper-content-wrapper]:not(.${codexPlusPageClass} *), html[data-codex-plus-page-open] [role="tooltip"]:not(.${codexPlusPageClass} *), html[data-codex-plus-page-open] [role="menu"]:not(.${codexPlusPageClass} *), @@ -2506,12 +2511,12 @@ function syncCodexPlusPagePopupStyle(open) { const id = `${styleId}-page-popups`; const existing = document.getElementById(id); - if (!open) { + if (!codexPlusIsWindowsPlatform || !open) { existing?.remove(); return; } if (existing) return; - // 仅在管理页打开时提升原生弹层的直接定位容器。即使根条件不命中, + // Windows 仅在管理页打开时提升原生弹层的直接定位容器。即使根条件不命中, // 常驻的 :has() 仍会增加聊天 DOM 变化时的样式失效开销。 const style = document.createElement("style"); style.id = id; diff --git a/assets/inject/renderer-inject/10-style.js b/assets/inject/renderer-inject/10-style.js index 1c85f2c810..a91ff45171 100644 --- a/assets/inject/renderer-inject/10-style.js +++ b/assets/inject/renderer-inject/10-style.js @@ -514,6 +514,11 @@ letter-spacing: -0.15px; white-space: nowrap; } + ${codexPlusIsWindowsPlatform ? "" : ` + html[data-codex-plus-page-open] div:has(> [data-radix-popper-content-wrapper], > [role="tooltip"], > [role="menu"]):not(.${codexPlusPageClass} *) { + z-index: 2147483645 !important; + } + `} html[data-codex-plus-page-open] [data-radix-popper-content-wrapper]:not(.${codexPlusPageClass} *), html[data-codex-plus-page-open] [role="tooltip"]:not(.${codexPlusPageClass} *), html[data-codex-plus-page-open] [role="menu"]:not(.${codexPlusPageClass} *), @@ -1833,12 +1838,12 @@ function syncCodexPlusPagePopupStyle(open) { const id = `${styleId}-page-popups`; const existing = document.getElementById(id); - if (!open) { + if (!codexPlusIsWindowsPlatform || !open) { existing?.remove(); return; } if (existing) return; - // 仅在管理页打开时提升原生弹层的直接定位容器。即使根条件不命中, + // Windows 仅在管理页打开时提升原生弹层的直接定位容器。即使根条件不命中, // 常驻的 :has() 仍会增加聊天 DOM 变化时的样式失效开销。 const style = document.createElement("style"); style.id = id; diff --git a/assets/inject/renderer-inject/manifest.json b/assets/inject/renderer-inject/manifest.json index 2b2ae80820..676d55b4c8 100644 --- a/assets/inject/renderer-inject/manifest.json +++ b/assets/inject/renderer-inject/manifest.json @@ -13,7 +13,7 @@ { "name": "10-style.js", "description": "样式注入、默认设置、后端设置映射、梦皮主题变量与持久化", - "lines": 2831 + "lines": 2836 }, { "name": "20-menu.js", diff --git a/assets/inject/upstream/dream-skin/macos/renderer-inject.js b/assets/inject/upstream/dream-skin/macos/renderer-inject.js index 99facaa24b..9a43ed07ac 100644 --- a/assets/inject/upstream/dream-skin/macos/renderer-inject.js +++ b/assets/inject/upstream/dream-skin/macos/renderer-inject.js @@ -61,23 +61,6 @@ // Cache structural :has() predicates as element markers. Chromium otherwise // re-evaluates these stylesheet conditions during every native hover update. const CSS_PREDICATES = [{"selector":":has([role=\"main\"])","attribute":"data-dream-has-0","branches":[{"selector":"[role=\"main\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-0]), :is(* [role=\"main\"]):not(*))"},{"selector":":has(main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"]))","attribute":"data-dream-has-1","branches":[{"selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","relation":"descendant"}],"replacement":":is(:where([data-dream-has-1]), :is(* main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])):not(*))"},{"selector":":has(main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"]):not([data-app-shell-active-page=\"false\"], [data-app-shell-active-page=\"false\"] *) [role=\"main\"])","attribute":"data-dream-has-2","branches":[{"selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"]):not([data-app-shell-active-page=\"false\"], [data-app-shell-active-page=\"false\"] *) [role=\"main\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-2]), :is(* main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"]):not([data-app-shell-active-page=\"false\"], [data-app-shell-active-page=\"false\"] *) [role=\"main\"]):not(*))"},{"selector":":has([data-testid=\"home-icon\"])","attribute":"data-dream-has-3","branches":[{"selector":"[data-testid=\"home-icon\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-3]), :is(* [data-testid=\"home-icon\"]):not(*))"},{"selector":":has(> .home-banners)","attribute":"data-dream-has-4","branches":[{"selector":".home-banners","relation":"child"}],"replacement":":is(:where([data-dream-has-4]), :is(* > .home-banners):not(*))"},{"selector":":has([data-ds-part=\"main\"])","attribute":"data-dream-has-5","branches":[{"selector":"[data-ds-part=\"main\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-5]), :is(* [data-ds-part=\"main\"]):not(*))"},{"selector":":has([data-ds-part=\"main\"], [data-ds-part=\"home\"])","attribute":"data-dream-has-6","branches":[{"selector":"[data-ds-part=\"main\"]","relation":"descendant"},{"selector":"[data-ds-part=\"home\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-6]), :is(* [data-ds-part=\"main\"], * [data-ds-part=\"home\"]):not(*))"},{"selector":":has(:is([class*=\"_homeUtilityBar_\"], [class*=\"_ComposerHomeUtilityBar_\"]))","attribute":"data-dream-has-7","branches":[{"selector":":is([class*=\"_homeUtilityBar_\"], [class*=\"_ComposerHomeUtilityBar_\"])","relation":"descendant"}],"replacement":":is(:where([data-dream-has-7]), :is(* :is([class*=\"_homeUtilityBar_\"], [class*=\"_ComposerHomeUtilityBar_\"])):not(*))"},{"selector":":has(input[type=\"text\"])","attribute":"data-dream-has-8","branches":[{"selector":"input[type=\"text\"]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-8]), :is(* input[type=\"text\"]):not(*))"},{"selector":":has(> input[type=\"text\"])","attribute":"data-dream-has-9","branches":[{"selector":"input[type=\"text\"]","relation":"child"}],"replacement":":is(:where([data-dream-has-9]), :is(* > input[type=\"text\"]):not(*))"},{"selector":":has([data-codex-composer-root])","attribute":"data-dream-has-10","branches":[{"selector":"[data-codex-composer-root]","relation":"descendant"}],"replacement":":is(:where([data-dream-has-10]), :is(* [data-codex-composer-root]):not(*))"},{"selector":":has(*)","attribute":"data-dream-has-11","branches":[{"selector":"*","relation":"descendant"}],"replacement":":is(:where([data-dream-has-11]), :is(* *):not(*))"},{"selector":":has(> .horizontal-scroll-fade-mask .group\\/project-selector)","attribute":"data-dream-has-12","branches":[{"selector":".horizontal-scroll-fade-mask .group\\/project-selector","relation":"child"}],"replacement":":is(:where([data-dream-has-12]), :is(* > .horizontal-scroll-fade-mask .group\\/project-selector):not(*))"},{"selector":":has(> [data-mention-list-scroll-area])","attribute":"data-dream-has-13","branches":[{"selector":"[data-mention-list-scroll-area]","relation":"child"}],"replacement":":is(:where([data-dream-has-13]), :is(* > [data-mention-list-scroll-area]):not(*))"},{"selector":":has(~ [role=\"dialog\"] [data-testid=\"image-preview-dismiss-area\"] table)","attribute":"data-dream-has-14","branches":[{"selector":"[role=\"dialog\"] [data-testid=\"image-preview-dismiss-area\"] table","relation":"sibling"}],"replacement":":is(:where([data-dream-has-14]), :is(* ~ [role=\"dialog\"] [data-testid=\"image-preview-dismiss-area\"] table):not(*))"},{"selector":":has([data-testid=\"image-preview-dismiss-area\"] table)","attribute":"data-dream-has-15","branches":[{"selector":"[data-testid=\"image-preview-dismiss-area\"] table","relation":"descendant"}],"replacement":":is(:where([data-dream-has-15]), :is(* [data-testid=\"image-preview-dismiss-area\"] table):not(*))"},{"selector":":has(table)","attribute":"data-dream-has-16","branches":[{"selector":"table","relation":"descendant"}],"replacement":":is(:where([data-dream-has-16]), :is(* table):not(*))"}] - .concat([ - // 补齐首页输入框和社区全图模式遗漏的条件;保留原 :has() 的权重。 - { - selector: ':has([data-composer-home-utility-bar-position="above"])', - attribute: "data-dream-has-home-utility-above", - owner: '[data-codex-composer-root][data-composer-placement="home"]', - branches: [{ selector: '[data-composer-home-utility-bar-position="above"]', relation: "descendant" }], - replacement: ':is(:where([data-dream-has-home-utility-above]), [data-composer-home-utility-bar-position="above"]:not(*))', - }, - { - selector: ':has(:is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]))', - attribute: "data-dream-has-timeline-composer", - owner: '[data-app-action-timeline-scroll] > [class~="pointer-events-none"][class~="absolute"]', - branches: [{ selector: ':is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"])', relation: "descendant" }], - replacement: ':is(:where([data-dream-has-timeline-composer]), :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]):not(*))', - }, - ]) .filter((item) => cssText.includes(item.selector)); for (const { selector, replacement } of CSS_PREDICATES) { cssText = cssText.replaceAll(selector, replacement); @@ -888,7 +871,7 @@ if (!CSS_PREDICATES.length) return; const startedAt = now(); metrics.predicatePasses += 1; - CSS_PREDICATES.forEach(({ selector, attribute, branches, owner }, index) => { + CSS_PREDICATES.forEach(({ selector, attribute, branches }, index) => { // Find the rare landmarks first. Running an unanchored :has() query over // every element repeats subtree searches, even when the landmark is absent. const candidates = new Set(); @@ -908,8 +891,7 @@ // Native editor contents never own these structural skin surfaces. // Their mutation observer normalizes unexpected attribute writes. const desired = new Set([...candidates].filter((node) => - !node.closest?.('[contenteditable="true"], .ProseMirror') && - (!owner || node.matches(owner)) && node.matches(selector))); + !node.closest?.('[contenteditable="true"], .ProseMirror') && node.matches(selector))); const previous = predicateNodes[index]; for (const node of previous) { if (!desired.has(node)) { @@ -1064,7 +1046,7 @@ // SPA route changes are observable as DOM mutations even when Chromium's // Navigation API emits no event. Keep verification scope and public parts // derived from the same post-mutation tree. - const predicateClass = /main-surface|_MainContentSurface_|home-banners|_homeUtilityBar_|_ComposerHomeUtilityBar_|horizontal-scroll-fade-mask|group\/project-selector|composer-surface-chrome|_ComposerLayoutRoot_|^pointer-events-none$|^absolute$/; + const predicateClass = /main-surface|_MainContentSurface_|home-banners|_homeUtilityBar_|_ComposerHomeUtilityBar_|horizontal-scroll-fade-mask|group\/project-selector/; partObserver = new MutationObserver((records) => { const changed = records.some((record) => { if (record.type !== "attributes") return true; @@ -1141,8 +1123,6 @@ "class", "role", "data-testid", "data-app-shell-main-surface", "data-app-shell-active-page", "data-codex-composer-root", "type", "data-mention-list-scroll-area", - "data-composer-placement", "data-composer-home-utility-bar-position", - "data-app-action-timeline-scroll", ], }); }; diff --git a/scripts/test-theme-shell-performance.cjs b/scripts/test-theme-shell-performance.cjs index 13162b8e65..2ef30cec87 100644 --- a/scripts/test-theme-shell-performance.cjs +++ b/scripts/test-theme-shell-performance.cjs @@ -23,7 +23,7 @@ async function run() { page.on("pageerror", error => errors.push(error.message)); // 只使用仓库自带资源,不访问个人会话或网络。 const artwork = "data:image/png;base64," + fs.readFileSync(path.join(root, "assets/inject/dream-skin-default.png")).toString("base64"); - for (const platform of ["windows", "macos"]) { + for (const platform of ["windows"]) { const css = [asset(`upstream/dream-skin/${platform}/dream-skin.css`), asset("dream-skin-native-shell-compat.css"), asset("dream-skin-community-compat.css")].join("\n"); let runtime = asset(`upstream/dream-skin/${platform}/renderer-inject.js`); @@ -132,13 +132,14 @@ async function run() { await state("win32", false); assert.deepEqual(await margins(), ["8px", "4px"]); } - console.log("Windows alignment: old/new structures, disabled theme and macOS passed"); + console.log("Windows alignment: old/new structures, disabled theme and non-Windows isolation passed"); const styles = asset("renderer-inject/10-style.js"); const pages = asset("renderer-inject/40-backend-settings.js"); await page.setContent('
'); await page.evaluate(`(() => { const styleId = "codex-delete-style"; + const codexPlusIsWindowsPlatform = true; const codexPlusPageClass = "codex-plus-page-overlay"; const codexPlusRailSelector = "nav[data-app-navigation-rail]"; const codexPlusRailDestinationSelector = "[data-sidebar-destination]"; From 2e5b7ebddbb7aa0e3059b29eb4fb18a48aee07ed Mon Sep 17 00:00:00 2001 From: xiaojie0815 Date: Tue, 10 Nov 2026 01:38:44 +0800 Subject: [PATCH 4/5] feat(theme): allow opt-in Windows shell layout in safe CSS --- .../inject/dream-skin-native-shell-compat.css | 12 --- .../codex-plus-core/src/dream_skin_package.rs | 90 +++++++++++++++---- .../tests/all/dream_skin_package.rs | 86 ++++++++++++++++++ docs/theme-shell-css.md | 32 +++++++ scripts/test-theme-shell-performance.cjs | 34 +++++-- 5 files changed, 215 insertions(+), 39 deletions(-) create mode 100644 docs/theme-shell-css.md diff --git a/assets/inject/dream-skin-native-shell-compat.css b/assets/inject/dream-skin-native-shell-compat.css index ccd2e86089..482312752b 100644 --- a/assets/inject/dream-skin-native-shell-compat.css +++ b/assets/inject/dream-skin-native-shell-compat.css @@ -6,16 +6,4 @@ :is([data-app-shell-page-surface="true"], [class*="_ApplicationMenuPageSurfaceLayout_"], [class~="bg-token-main-surface-primary"][class~="h-full"][class~="w-full"]):not([data-codex-composer-root], [data-codex-composer-root] *, [role="dialog"], [role="dialog"] *, [role="menu"], [role="menu"] *, [data-slot="popover-content"], [data-slot="popover-content"] *) { background: transparent !important; } - - /* 26.1103 的侧栏位于 workspace 内,header 则在 main 外。 - 使用公开部件标记适配两种层级,消除原生 8px/4px 留白造成的皮肤断层。 */ - html[data-dream-skin="active"][data-codex-os="win32"][data-codex-window-chrome="application-menu"] - [data-ds-part="sidebar"] { - margin-top: 0 !important; - } - - html[data-dream-skin="active"][data-codex-os="win32"][data-codex-window-chrome="application-menu"] - [data-ds-part="header"] { - margin-inline-end: 0 !important; - } } diff --git a/crates/codex-plus-core/src/dream_skin_package.rs b/crates/codex-plus-core/src/dream_skin_package.rs index 52a92b53af..c1d417c511 100644 --- a/crates/codex-plus-core/src/dream_skin_package.rs +++ b/crates/codex-plus-core/src/dream_skin_package.rs @@ -222,26 +222,35 @@ pub fn compile_safe_css(css: &str) -> anyhow::Result { }) .collect::>(); let mut compiled = String::from("@layer dreamskin-community {\n"); - for rule in rules { - compiled.push_str(" "); - compiled.push_str(rule.selector); - compiled.push_str(" {\n"); - for (property, value) in &rule.declarations { - compiled.push_str(" "); - compiled.push_str(property); - compiled.push_str(": "); - compiled.push_str(value); - compiled.push_str(" !important;\n"); - if rule.part == "composer" && composer_base_borders.contains(property) { - compiled.push_str(&format!( - " --ds-community-composer-{property}: {value} !important;\n" - )); - } - if *property == "background-color" && matches!(rule.part, "sidebar" | "main" | "home") { - compiled.push_str(" background-image: none !important;\n"); + for rule in &rules { + if rule + .declarations + .iter() + .any(|(property, _)| !is_shell_layout_property(property)) + { + compiled.push_str(" "); + compiled.push_str(rule.selector); + compiled.push_str(" {\n"); + for (property, value) in &rule.declarations { + if is_shell_layout_property(property) { + continue; + } + compiled.push_str(" "); + compiled.push_str(property); + compiled.push_str(": "); + compiled.push_str(value); + compiled.push_str(" !important;\n"); + if rule.part == "composer" && composer_base_borders.contains(property) { + compiled.push_str(&format!( + " --ds-community-composer-{property}: {value} !important;\n" + )); + } + if *property == "background-color" && matches!(rule.part, "sidebar" | "main" | "home") { + compiled.push_str(" background-image: none !important;\n"); + } } + compiled.push_str(" }\n"); } - compiled.push_str(" }\n"); if rule.part == "home-hero" && let Some((_, value)) = rule @@ -294,9 +303,51 @@ pub fn compile_safe_css(css: &str) -> anyhow::Result { } } compiled.push_str("}\n"); + + // 仅编译主题显式声明的 Windows shell 几何属性。原生 utilities 的 + // !important 优先于后声明的 community 层,故使用已有 theme 层覆盖。 + // 其他外观声明仍在 community 层,非 Windows 平台不接收这些布局设置。 + let mut shell_layout = String::new(); + for rule in &rules { + let declarations = rule + .declarations + .iter() + .filter(|(property, _)| is_shell_layout_property(property)) + .collect::>(); + if declarations.is_empty() { + continue; + } + shell_layout.push_str(&format!( + " html[data-dream-skin=\"active\"][data-codex-os=\"win32\"] {} {{\n", + rule.selector + )); + for (property, value) in declarations { + shell_layout.push_str(&format!(" {property}: {value} !important;\n")); + } + shell_layout.push_str(" }\n"); + } + if !shell_layout.is_empty() { + compiled.push_str("@layer theme {\n"); + compiled.push_str(&shell_layout); + compiled.push_str("}\n"); + } Ok(compiled) } +fn is_shell_layout_property(property: &str) -> bool { + matches!(property, "margin-top" | "margin-inline-end" | "height") +} + +fn validate_shell_layout_value(part: &str, property: &str, value: &str) -> bool { + match (part, property) { + ("sidebar", "margin-top") | ("header", "margin-inline-end") => { + zero_or_px(value, 0.0, 64.0) + } + ("header", "height") => numeric(value, "px", 32.0, 128.0), + _ => false, + } +} + #[derive(Debug)] struct SafeCssRule<'a> { selector: &'a str, @@ -391,7 +442,8 @@ fn parse_safe_css(css: &str) -> anyhow::Result>> { if value.is_empty() || value.chars().count() > 512 || value.contains(['{', '}', '<', '>', '!', ';']) - || !validate_property_value(property, value) + || !(validate_property_value(property, value) + || validate_shell_layout_value(part, property, value)) { bail!("theme.css 属性值不受 Safe CSS 支持:{property}"); } diff --git a/crates/codex-plus-core/tests/all/dream_skin_package.rs b/crates/codex-plus-core/tests/all/dream_skin_package.rs index cb25399c57..eafdaae591 100644 --- a/crates/codex-plus-core/tests/all/dream_skin_package.rs +++ b/crates/codex-plus-core/tests/all/dream_skin_package.rs @@ -189,6 +189,92 @@ fn safe_css_matches_the_dreamskin_value_policy() { assert!(toolbar.contains("button:not([class~=\"bg-token-foreground\"]")); } +#[test] +fn shell_layout_css_is_opt_in_and_windows_scoped() { + let unchanged = compile_safe_css("[data-ds-part=\"sidebar\"] { color: #fff; }").unwrap(); + assert!(!unchanged.contains("@layer theme {")); + assert!(!unchanged.contains("margin-top")); + + let aligned = compile_safe_css( + "[data-ds-part=\"sidebar\"] { margin-top: 0; }\n\ + [data-ds-part=\"header\"] { margin-inline-end: 0; height: 52px; }", + ) + .unwrap(); + let custom = compile_safe_css( + "[data-ds-part=\"sidebar\"] { margin-top: 6px; color: #fff; }\n\ + [data-ds-part=\"header\"] { margin-inline-end: 12px; height: 64px; }", + ) + .unwrap(); + let (appearance, layout) = custom.split_once("@layer theme {\n").unwrap(); + assert!(appearance.contains("color: #fff !important;")); + assert!(!appearance.contains("margin-top")); + assert!(!appearance.contains("height:")); + assert!(layout.contains( + "html[data-dream-skin=\"active\"][data-codex-os=\"win32\"] [data-ds-part=\"sidebar\"]" + )); + assert!(layout.contains("margin-top: 6px !important;")); + assert!(layout.contains("margin-inline-end: 12px !important;")); + assert!(layout.contains("height: 64px !important;")); + + // 可选导出实际编译结果,供 Chromium 回归验证原生 important 的层叠与切换恢复。 + if let Some(path) = std::env::var_os("CODEX_THEME_LAYOUT_CSS_OUTPUT") { + std::fs::write( + path, + serde_json::to_vec(&json!({ + "unchanged": unchanged, + "aligned": aligned, + "custom": custom, + })) + .unwrap(), + ) + .unwrap(); + } +} + +#[test] +fn shell_layout_css_validates_parts_units_and_bounds() { + for css in [ + "[data-ds-part=\"sidebar\"] { margin-top: 0px; }", + "[data-ds-part=\"sidebar\"] { margin-top: 64px; }", + "[data-ds-part=\"header\"] { margin-inline-end: 0; height: 32px; }", + "[data-ds-part=\"header\"] { margin-inline-end: 64px; height: 128px; }", + ] { + assert!(compile_safe_css(css).is_ok(), "rejected layout CSS: {css}"); + } + for css in [ + "[data-ds-part=\"root\"] { height: 52px; }", + "[data-ds-part=\"composer\"] { height: 52px; }", + "[data-ds-part=\"header\"] { margin-top: 8px; }", + "[data-ds-part=\"sidebar\"] { margin-inline-end: 8px; }", + "[data-ds-part=\"sidebar\"] { margin-top: -8px; }", + "[data-ds-part=\"sidebar\"] { margin-top: 65px; }", + "[data-ds-part=\"header\"] { margin-inline-end: 8%; }", + "[data-ds-part=\"header\"] { height: 0; }", + "[data-ds-part=\"header\"] { height: 31px; }", + "[data-ds-part=\"header\"] { height: 129px; }", + "[data-ds-part=\"header\"] { height: calc(50px + 2px); }", + "[data-ds-part=\"header\"] { height: var(--native-height); }", + "[data-ds-part=\"sidebar\"] { margin-top: 0 !important; }", + ] { + assert!(compile_safe_css(css).is_err(), "accepted invalid layout CSS: {css}"); + } +} + +#[test] +fn installed_package_preserves_and_activates_shell_layout_css() { + let temp = tempfile::tempdir().unwrap(); + let css = br#"[data-ds-part="sidebar"] { margin-top: 6px; } +[data-ds-part="header"] { margin-inline-end: 12px; height: 64px; }"#; + let package = validate_and_read_package(&package_bytes("windows", css, None), "windows").unwrap(); + save_validated_dream_skin_package(temp.path(), &package).unwrap(); + let stored = load_stored_dream_skin_theme(temp.path(), "community.theme").unwrap(); + prepare_dream_skin_activation(temp.path(), &stored).unwrap(); + assert_eq!( + std::fs::read(temp.path().join("dream-skin/theme/current.css")).unwrap(), + css + ); +} + #[test] fn installed_package_preserves_and_activates_safe_css() { let temp = tempfile::tempdir().unwrap(); diff --git a/docs/theme-shell-css.md b/docs/theme-shell-css.md new file mode 100644 index 0000000000..94d7ebbdf5 --- /dev/null +++ b/docs/theme-shell-css.md @@ -0,0 +1,32 @@ +# Windows 主题 CSS 的标题栏高度和边距 + +新版 Codex 的侧栏和聊天标题栏结构发生变化。主题作者需要在 `theme.css` 中调整相应部件的高度、边距;原有 Safe CSS 校验不接受这些属性。本扩展提供按主题配置的入口,未声明时沿用原有布局。 + +本次仅开放以下 Windows shell 属性: + +| Skin API 部件 | 属性 | 支持值 | +| --- | --- | --- | +| `sidebar` | `margin-top` | `0` 或 `0px`~`64px` | +| `header` | `margin-inline-end` | `0` 或 `0px`~`64px` | +| `header` | `height` | `32px`~`128px` | + +例如,主题作者可选择: + +```css +[data-ds-part="sidebar"] { + margin-top: 0; +} + +[data-ds-part="header"] { + margin-inline-end: 0; + height: 52px; +} +``` + +这些只是示例数值,不会自动写入任何主题。也可使用不同的合法数值,或删除相应声明以恢复原有布局。 + +将声明放入主题包的 `theme.css`,更新 manifest 中对应文件的大小与 SHA-256 后,按原有流程导入、保存及应用主题。无需 `!important`,客户端会在校验通过后添加覆盖优先级。Safe CSS 文件不接受注释。 + +布局声明只在主题启用且运行于 Windows 时生效。颜色、字体等原有外观声明仍沿用原来的跨平台规则。没有改动 macOS renderer,也没有 macOS 实机测试结果。 + +回归测试可先通过 `cargo test --release -p codex-plus-core --test all dream_skin_package::` 验证校验、包导入和保存。设置 `CODEX_THEME_LAYOUT_CSS_OUTPUT` 为一个可写 JSON 文件路径后,该测试会导出实际编译结果;沿用此环境变量运行 `node scripts/test-theme-shell-performance.cjs`,可在浏览器中验证自定义数值、原生 important 层叠、换肤及关闭主题后的恢复。 diff --git a/scripts/test-theme-shell-performance.cjs b/scripts/test-theme-shell-performance.cjs index 2ef30cec87..c7645d228e 100644 --- a/scripts/test-theme-shell-performance.cjs +++ b/scripts/test-theme-shell-performance.cjs @@ -14,6 +14,8 @@ const extract = (source, name) => { }; const homeMarker = "data-dream-has-home-utility-above"; const timelineMarker = "data-dream-has-timeline-composer"; +const layoutFixtures = process.env.CODEX_THEME_LAYOUT_CSS_OUTPUT + ? JSON.parse(fs.readFileSync(process.env.CODEX_THEME_LAYOUT_CSS_OUTPUT, "utf8")) : null; async function run() { const browser = await chromium.launch({ headless: true, executablePath: process.env.CODEX_TEST_BROWSER_PATH }); @@ -111,28 +113,44 @@ async function run() { // 新版 header 在 main 外,aside 在 workspace 内;同时保留旧版结构。 for (const structure of ["legacy", "workspace"]) { await page.setContent(`${structure === "workspace" ? '
' : '
'}`); await page.addStyleTag({content: asset("dream-skin-native-shell-compat.css")}); const margins = () => page.evaluate(() => [getComputedStyle(document.getElementById("sidebar")).marginTop, - getComputedStyle(document.getElementById("header")).marginInlineEnd]); + getComputedStyle(document.getElementById("header")).marginInlineEnd, + getComputedStyle(document.getElementById("header")).height]); const state = (os, active) => page.evaluate(({os,active}) => { Object.assign(document.documentElement.dataset, {codexOs:os, codexWindowChrome:"application-menu"}); if (active) document.documentElement.dataset.dreamSkin = "active"; else delete document.documentElement.dataset.dreamSkin; }, {os, active}); await state("win32", true); - assert.deepEqual(await margins(), ["0px", "0px"]); + assert.deepEqual(await margins(), ["8px", "4px", "52px"], "no theme layout declaration preserves native geometry"); assert.deepEqual(await page.$eval("#header", n => [getComputedStyle(n).backgroundColor, getComputedStyle(n).backdropFilter]), ["rgb(11, 22, 33)", "blur(12px)"]); - await state("darwin", true); - assert.deepEqual(await margins(), ["8px", "4px"]); - await state("win32", false); - assert.deepEqual(await margins(), ["8px", "4px"]); + if (layoutFixtures) { + const unchanged = await page.addStyleTag({content: layoutFixtures.unchanged}); + assert.deepEqual(await margins(), ["8px", "4px", "52px"]); + await unchanged.evaluate(n => n.remove()); + for (const [name, expected] of [["custom", ["6px", "12px", "64px"]], ["aligned", ["0px", "0px", "52px"]]]) { + const style = await page.addStyleTag({content: layoutFixtures[name]}); + await state("win32", true); + assert.deepEqual(await margins(), expected, `${structure}/${name}: authored values override native important utilities`); + await state("darwin", true); + assert.deepEqual(await margins(), ["8px", "4px", "52px"], "Windows layout declarations are isolated"); + await state("win32", false); + assert.deepEqual(await margins(), ["8px", "4px", "52px"], "disabled theme restores native geometry"); + await state("win32", true); + await style.evaluate(n => n.remove()); + assert.deepEqual(await margins(), ["8px", "4px", "52px"], "switching to an unconfigured theme restores defaults"); + } + } } - console.log("Windows alignment: old/new structures, disabled theme and non-Windows isolation passed"); + console.log(layoutFixtures + ? "Windows theme layout: actual Rust-compiled CSS, custom values, theme switch, disable and platform isolation passed" + : "Windows theme layout: native defaults passed; compiled CSS checks require CODEX_THEME_LAYOUT_CSS_OUTPUT"); const styles = asset("renderer-inject/10-style.js"); const pages = asset("renderer-inject/40-backend-settings.js"); From 34a5d10b6296ee302ddf119ba945537670c0612e Mon Sep 17 00:00:00 2001 From: BigPizzaV3 <2649340134@qq.com> Date: Tue, 10 Nov 2026 21:18:54 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20Windows=20=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E4=B8=8A=E6=B8=B8=E8=B5=84=E4=BA=A7=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E5=B9=B6=E5=90=8C=E6=AD=A5=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/codex-plus-core/tests/all/upstream_theme_assets.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/codex-plus-core/tests/all/upstream_theme_assets.rs b/crates/codex-plus-core/tests/all/upstream_theme_assets.rs index 912a4922de..32a451cba9 100644 --- a/crates/codex-plus-core/tests/all/upstream_theme_assets.rs +++ b/crates/codex-plus-core/tests/all/upstream_theme_assets.rs @@ -31,7 +31,7 @@ fn bundled_target_renderers_and_styles_remain_byte_exact() { for (path, hash) in [ ( "assets/inject/upstream/dream-skin/windows/renderer-inject.js", - "576C52AF236D251E815C3398A8A8DA419287E1281EBF2E05718DDD9FADB248CC", + "3588A378FB01FC885123CA837F37F23E04C7B2804A72AF6490729907002869D3", ), ( "assets/inject/upstream/dream-skin/windows/dream-skin.css",