From c4c3a7e90c0def8c4a6961ca013ec2ac36ca15b8 Mon Sep 17 00:00:00 2001 From: huangjinnan_mac Date: Tue, 18 Aug 2026 20:47:20 +0800 Subject: [PATCH 1/5] fix(renderer): exclude pet surfaces and restore composer root Prevent Dream Skin from injecting wallpaper and theme state into avatar overlay and Pet composition renderers. Clean and verify already polluted auxiliary targets in both one-shot and watcher discovery paths. Recognize the current ComposerLayoutRoot and toolbar CSS-module prefixes, while retaining legacy selectors and a guarded semantic fallback. Add shared renderer and dual-platform injector regressions and regenerate runtime assets. --- TASK_PROGRESS.md | 51 +++++++++++++++++++ macos/assets/dream-skin.css | 26 +++++----- macos/assets/renderer-inject.js | 17 ++++++- macos/assets/selectors.json | 4 +- macos/scripts/injector.mjs | 28 ++++++++-- macos/tests/injector-bootstrap.test.mjs | 29 ++++++++++- runtime/renderer-inject.js | 15 ++++++ tools/renderer-runtime.test.mjs | 62 ++++++++++++++++++++--- tools/selectors.json | 4 +- windows/assets/dream-skin.css | 26 +++++----- windows/assets/renderer-inject.js | 17 ++++++- windows/assets/selectors.json | 4 +- windows/scripts/injector.mjs | 25 ++++++++- windows/tests/injector-bootstrap.test.mjs | 29 ++++++++++- 14 files changed, 288 insertions(+), 49 deletions(-) diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index 9f538f60..0c3c82a8 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -1,5 +1,56 @@ # Task Progress +## Codex composer and Pet surface compatibility (2026-08-18) + +- [goal] Fix current Codex renderer drift where the composer fallback binds to + `_ComposerLayoutFooter_` instead of `_ComposerLayoutRoot_`, and prevent Dream + Skin from injecting wallpaper/CSS into `/avatar-overlay` and Pet composition + surfaces that must remain transparent. +- [scope] Isolated clone + `/Users/huangjinnan/Desktop/Codex-Dream-Skin-pr-composer-pet-fix`, branch + `codex/fix-composer-pet-scope`, based on exact `origin/main@95423d8` (v1.5.14). + Shared runtime source, generated dual-platform assets, focused regressions, + and this progress record are in scope. User-specific theme colors are not. +- [field evidence] Live macOS Codex exposed `_ComposerLayoutRoot_*` around the + active ProseMirror while the v1.5.14 fallback marked `_ComposerLayoutFooter_*`. + The avatar overlay plus Pet composition surfaces carried + `data-dream-skin="active"`; three bodies had the theme wallpaper instead of a + transparent background. Local runtime guards and explicit cleanup restored + all five Pet-related surfaces to transparent without changing Pet assets. +- [implemented] Canonical selector contract now accepts both legacy + `.composer-surface-chrome` and `_ComposerLayoutRoot_`, with the matching + toolbar selector. The generic fallback also prefers `_ComposerLayoutRoot_` + before broad composer/prompt ownership so `_ComposerLayoutFooter_` cannot + capture the public composer part. +- [implemented] Shared renderer payload self-excludes `/avatar-overlay` and + Pet composition-surface routes before installing style/art state, cleaning a + previous payload first when present. Both platform injectors classify those + targets explicitly, and one-shot plus watcher discovery reuse the existing + remove-and-verify path to clean already contaminated Pet renderers. +- [tests] Shared renderer regressions cover fresh Pet exclusion, cleanup after + prior injection, ComposerLayoutRoot selection, and footer non-selection. + macOS/Windows bootstrap regressions cover probe exclusion and require both + discovery paths to call verified cleanup. Focused dual-platform renderer, + bootstrap, selector-doctor, syntax, sync, payload, and diff checks pass. +- [tests] Portable Node set passes 102/103 in the default sandbox; the sole + Swift bounded-HTTP test was blocked by module-cache permission/toolchain + setup there and passes when rerun outside the sandbox, yielding 103/103 + covered tests. The complete macOS wrapper passes outside the sandbox with + its documented signed-runtime, full-Xcode, and Doctor branches skipped. +- [skill] User Skill `~/.codex/skills/dream-skin-repair` is installed. Its + read-only audit reports the main renderer themed, all five Pet-related + targets transparent/unmarked, and no anomalies; `quick_validate.py` passes. +- [review] Independent code/spec/security review found no Critical, High, + Medium, or Low issues. It confirmed renderer self-exclusion, verified cleanup + in one-shot and watcher paths, ComposerLayoutRoot selection, generated asset + synchronization, and focused dual-platform coverage. +- [current] Implementation, generated assets, tests, and review are complete. + No commit, push, or PR yet. Fork `hjnnjh/Codex-Dream-Skin` now exists for the + contribution branch. +- [next] Inspect and stage the final diff, create an atomic commit, push the + fork branch, and open a Ready PR with exact test/gap evidence. Native Windows + PowerShell 5.1/7 remains a CI gate. + ## Issue #352 fix and v1.5.14 release (2026-08-12) - [fix merged] PR #360 (`e3787857953998a1916c39b10942ac6c15978a25`) passed exact-head CI run `31558654733`: Static, macOS repository regressions plus universal DMG, Windows PowerShell 7, and Windows PowerShell 5.1 plus Setup.exe. It was squash-merged with the authorized same-owner review bypass at `2026-08-12T03:06:37Z` as `main@69a5a2e4b68174b1c0c70a2fa62adf1aca1eff2a`. diff --git a/macos/assets/dream-skin.css b/macos/assets/dream-skin.css index 939ce094..86dcfbf8 100644 --- a/macos/assets/dream-skin.css +++ b/macos/assets/dream-skin.css @@ -428,7 +428,7 @@ html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream- } html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main:is(.main-surface, [data-app-shell-main-surface], [class*="_MainContentSurface_"])):not(:has(main:is(.main-surface, [data-app-shell-main-surface], [class*="_MainContentSurface_"]) [role="main"])) - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; box-shadow: @@ -890,7 +890,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } -html[data-dream-skin="active"] .composer-surface-chrome { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { overflow: visible !important; border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important; border-radius: 22px !important; @@ -899,7 +899,7 @@ html[data-dream-skin="active"] .composer-surface-chrome { backdrop-filter: blur(16px) saturate(108%) !important; } -html[data-dream-skin="active"] .composer-surface-chrome::before { content: none !important; } +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"])::before { content: none !important; } /* Newer app shells may no longer expose the selector contract above. The renderer assigns these parts only from one coherent main/composer/sidebar @@ -1082,7 +1082,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main:is(.main-surface backdrop-filter: blur(8px) saturate(104%) !important; } -html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chrome { +html[data-dream-skin="active"][data-dream-art-wide="true"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; box-shadow: @@ -1093,7 +1093,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chr } html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .14), inset 0 0 0 1px var(--ds-immersive-line), @@ -1102,7 +1102,7 @@ html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="tr } html[data-dream-skin="active"][data-dream-shell="light"] - .composer-surface-chrome p.placeholder::after { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .78) !important; opacity: 1 !important; } @@ -1125,7 +1125,7 @@ html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { } html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { border: 0 !important; border-radius: 0 0 22px 22px !important; box-shadow: @@ -1145,7 +1145,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtility } html[data-dream-skin="active"][data-dream-art-wide="true"] - [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { + [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { box-shadow: 0 10px 30px rgb(var(--ds-bg-rgb) / .20), inset 1px 0 var(--ds-immersive-line), @@ -1163,27 +1163,27 @@ html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="tr } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]) { color: var(--ds-muted) !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button svg, -html[data-dream-skin="active"] .composer-surface-chrome button svg { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button svg { color: currentColor !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button *, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) * { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]) * { color: currentColor !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button:hover, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]):hover { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]):hover { color: var(--ds-text) !important; background: rgb(var(--ds-accent-rgb) / .10) !important; } -html[data-dream-skin="active"] .composer-surface-chrome p.placeholder::after { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .82) !important; opacity: 1 !important; } diff --git a/macos/assets/renderer-inject.js b/macos/assets/renderer-inject.js index 6076e7ab..45b02e56 100644 --- a/macos/assets/renderer-inject.js +++ b/macos/assets/renderer-inject.js @@ -1,6 +1,6 @@ // Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. ((cssText, artDataUrl, themeConfig) => { - const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"settings-panel","selector":"[data-settings-panel-slug=\"general-settings\"]","tier":"L2","scope":"settings","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":":is(.composer-surface-chrome, [class*=\"_ComposerLayoutRoot_\"])","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":":is(.composer-surface-chrome [class*=\"_footer_\"], [class*=\"_ComposerLayoutRoot_\"] [class*=\"_ComposerLayoutFooter_\"])","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"settings-panel","selector":"[data-settings-panel-slug=\"general-settings\"]","tier":"L2","scope":"settings","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; @@ -13,6 +13,19 @@ "data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect", "data-dream-art-ready", ]; + const initialRoute = new URLSearchParams(String(location.search || "")) + .get("initialRoute") || ""; + const pathname = String(location.pathname || ""); + const excludedPetSurface = location.protocol === "app:" && ( + pathname.endsWith("/avatar-overlay-composition-surface.html") || + initialRoute === "/avatar-overlay" || initialRoute.startsWith("/avatar-overlay/") + ); + if (excludedPetSurface) { + const previous = window[STATE_KEY]; + if (typeof previous?.cleanup === "function") previous.cleanup(); + window[DISABLED_KEY] = true; + return; + } const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; const PAYLOAD_REVISION = __DREAM_SKIN_PAYLOAD_REVISION_JSON__; @@ -670,6 +683,8 @@ const main = resolvedMainNode(); for (const input of genericInputNodes()) { if (main && !main.contains?.(input)) continue; + const layoutRoot = input.closest?.('[class*="_ComposerLayoutRoot_"]'); + if (layoutRoot && (!main || main.contains?.(layoutRoot))) return [layoutRoot]; const owner = input.closest?.( '[data-testid*="composer" i], [data-testid*="prompt" i], ' + '[class*="composer" i], [class*="prompt" i]', diff --git a/macos/assets/selectors.json b/macos/assets/selectors.json index 98347146..17e78a79 100644 --- a/macos/assets/selectors.json +++ b/macos/assets/selectors.json @@ -35,8 +35,8 @@ { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, - { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, - { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, + { "key": "composer-chrome", "selector": ":is(.composer-surface-chrome, [class*=\"_ComposerLayoutRoot_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。兼容旧语义类与新版 CSS Modules 根容器;hash 随版本变化。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, + { "key": "composer-toolbar", "selector": ":is(.composer-surface-chrome [class*=\"_footer_\"], [class*=\"_ComposerLayoutRoot_\"] [class*=\"_ComposerLayoutFooter_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;兼容旧 footer 前缀与新版 ComposerLayoutFooter CSS Module,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, diff --git a/macos/scripts/injector.mjs b/macos/scripts/injector.mjs index 186ffe69..b8d9e09c 100644 --- a/macos/scripts/injector.mjs +++ b/macos/scripts/injector.mjs @@ -510,6 +510,13 @@ async function listAppTargets(port) { async function probeSession(session) { return session.evaluate(`(() => { + const initialRoute = new URLSearchParams(String(location.search || '')) + .get('initialRoute') || ''; + const pathname = String(location.pathname || ''); + const excludedPetSurface = location.protocol === 'app:' && ( + pathname.endsWith('/avatar-overlay-composition-surface.html') || + initialRoute === '/avatar-overlay' || initialRoute.startsWith('/avatar-overlay/') + ); const genericCodexSurface = () => { if (location.protocol !== 'app:') return false; const main = document.querySelector('main, [role="main"]'); @@ -531,7 +538,8 @@ async function probeSession(session) { Boolean(document.querySelector(${stableTestidLiteral("theme-preview")})); return { markers, - codex: location.protocol === 'app:' && + excludedPetSurface, + codex: !excludedPetSurface && location.protocol === 'app:' && ((markers.shell && markers.sidebar) || settings || markers.main || markers.generic), }; })()`); @@ -565,7 +573,12 @@ async function connectCodexTargets(port, timeoutMs) { session = await connectTarget(target, port); const probe = await probeSession(session); if (probe?.codex) connected.push({ target, session, probe }); - else session.close(); + else { + if (probe?.excludedPetSurface && !await cleanupExcludedSurface(session)) { + throw new Error("Excluded Pet surface cleanup did not verify"); + } + session.close(); + } } catch (error) { session?.close(); lastError = error; @@ -1082,6 +1095,11 @@ async function verifyRemovedSession(session) { })()`); } +export async function cleanupExcludedSurface(session) { + if (!await removeFromSession(session)) return false; + return verifyRemovedSession(session); +} + export async function inspectNativeWindow(session) { try { const response = await session.send( @@ -1997,12 +2015,16 @@ async function runWatch(options) { const probe = await waitForCodexProbe(session); if (!probe?.codex) { await removeEarly(record); + if (probe?.excludedPetSurface && !await cleanupExcludedSurface(session)) { + throw new Error("Excluded Pet surface cleanup did not verify"); + } session.close(); sessions.delete(target.id); - if (!rejected.has(target.id)) { + if (!probe?.excludedPetSurface && !rejected.has(target.id)) { console.error(`[dream-skin] rejected non-ChatGPT app target ${target.id}`); rejected.add(target.id); } + if (probe?.excludedPetSurface) rejected.delete(target.id); continue; } rejected.delete(target.id); diff --git a/macos/tests/injector-bootstrap.test.mjs b/macos/tests/injector-bootstrap.test.mjs index 6e4861a7..75d1e2d3 100644 --- a/macos/tests/injector-bootstrap.test.mjs +++ b/macos/tests/injector-bootstrap.test.mjs @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import vm from "node:vm"; import { fileURLToPath } from "node:url"; -import { earlyPayloadFor } from "../scripts/injector.mjs"; +import { cleanupExcludedSurface, earlyPayloadFor } from "../scripts/injector.mjs"; const here = path.dirname(fileURLToPath(import.meta.url)); const injectorPath = path.resolve(here, "../scripts/injector.mjs"); @@ -149,8 +149,14 @@ const liveProbePayload = vm.runInNewContext(`\`${probeTemplate}\``, { const runLiveProbe = ({ protocol = "app:", settingsPanel: hasSettingsPanel = false, genericMain = false, genericInput = false, branding = false, + pathname = "/index.html", initialRoute = "", } = {}) => vm.runInNewContext(liveProbePayload, { - location: { protocol }, + location: { + protocol, + pathname, + search: initialRoute ? `?initialRoute=${encodeURIComponent(initialRoute)}` : "", + }, + URLSearchParams, document: { querySelector(selector) { if (selector === "[selector-settings-panel]") return hasSettingsPanel ? {} : null; @@ -173,6 +179,25 @@ assert.equal(runLiveProbe({ genericMain: true, genericInput: true }).codex, fals "The live probe must reject an unbranded generic app target."); assert.equal(runLiveProbe({ genericMain: true, genericInput: true, branding: true }).codex, true, "The live probe may accept generic anchors only with the stable Codex branding marker."); +const avatarOverlayProbe = runLiveProbe({ settingsPanel: true, initialRoute: "/avatar-overlay" }); +assert.equal(avatarOverlayProbe.excludedPetSurface, true); +assert.equal(avatarOverlayProbe.codex, false, + "The avatar overlay must never be treated as the primary Codex renderer."); +const petCompositionProbe = runLiveProbe({ + settingsPanel: true, pathname: "/avatar-overlay-composition-surface.html", +}); +assert.equal(petCompositionProbe.excludedPetSurface, true); +assert.equal(petCompositionProbe.codex, false, + "Pet composition surfaces must stay outside the Dream Skin target set."); +const cleanupEvaluations = []; +assert.equal(await cleanupExcludedSurface({ + async evaluate(expression) { cleanupEvaluations.push(expression); return true; }, +}), true, "Excluded Pet cleanup must remove and verify stale renderer state."); +assert.equal(cleanupEvaluations.length, 2); +assert.match(cleanupEvaluations[0], /__CODEX_DREAM_SKIN_DISABLED__/); +assert.match(cleanupEvaluations[1], /hasAttributes/); +assert.ok((source.match(/probe\?\.excludedPetSurface && !await cleanupExcludedSurface/g) || []).length >= 2, + "One-shot and watcher discovery must both clean excluded Pet targets."); assert.match(identityProbeSource, /selectorLiteral\("settings-panel"\)/, "The live probe must retain the current Settings structural marker."); assert.match(identityProbeSource, /return Boolean\(main && input && branded\)/, diff --git a/runtime/renderer-inject.js b/runtime/renderer-inject.js index aa81ac14..a91a68e1 100644 --- a/runtime/renderer-inject.js +++ b/runtime/renderer-inject.js @@ -13,6 +13,19 @@ "data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect", "data-dream-art-ready", ]; + const initialRoute = new URLSearchParams(String(location.search || "")) + .get("initialRoute") || ""; + const pathname = String(location.pathname || ""); + const excludedPetSurface = location.protocol === "app:" && ( + pathname.endsWith("/avatar-overlay-composition-surface.html") || + initialRoute === "/avatar-overlay" || initialRoute.startsWith("/avatar-overlay/") + ); + if (excludedPetSurface) { + const previous = window[STATE_KEY]; + if (typeof previous?.cleanup === "function") previous.cleanup(); + window[DISABLED_KEY] = true; + return; + } const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; const PAYLOAD_REVISION = __DREAM_SKIN_PAYLOAD_REVISION_JSON__; @@ -670,6 +683,8 @@ const main = resolvedMainNode(); for (const input of genericInputNodes()) { if (main && !main.contains?.(input)) continue; + const layoutRoot = input.closest?.('[class*="_ComposerLayoutRoot_"]'); + if (layoutRoot && (!main || main.contains?.(layoutRoot))) return [layoutRoot]; const owner = input.closest?.( '[data-testid*="composer" i], [data-testid*="prompt" i], ' + '[class*="composer" i], [class*="prompt" i]', diff --git a/tools/renderer-runtime.test.mjs b/tools/renderer-runtime.test.mjs index ef635daf..1929a660 100644 --- a/tools/renderer-runtime.test.mjs +++ b/tools/renderer-runtime.test.mjs @@ -30,7 +30,8 @@ function classList(initial) { function makeFixture({ nativeAppearance = "dark", settings = false, settingsPanel = false, adopted = true, generic = false, genericComposer = true, genericHome = false, genericSearch = false, - modernMessages = false, + modernMessages = false, modernComposerLayout = false, + pathname = "/index.html", initialRoute = "", } = {}) { const attrs = new Map(); const rootStyle = styleDeclaration(); @@ -103,15 +104,22 @@ function makeFixture({ const sidebarSelector = 'aside, nav[aria-label]'; const composerSelector = '[data-testid*="composer" i], [data-testid*="prompt" i], ' + '[class*="composer" i], [class*="prompt" i]'; + const composerLayoutRootSelector = '[class*="_ComposerLayoutRoot_"]'; const overlaySelector = '[role="dialog"], [aria-modal="true"]'; partFixtures.shell = makeDomNode("generic-shell", body); partFixtures.sidebar = makeDomNode("generic-sidebar", partFixtures.shell, new Map(), [sidebarSelector]); partFixtures.main = makeDomNode("generic-main", partFixtures.shell, new Map(), [mainSelector]); if (genericComposer) { partFixtures.composer = makeDomNode( - "generic-composer", partFixtures.main, new Map(), [composerSelector], + "generic-composer", partFixtures.main, new Map(), + [modernComposerLayout ? composerLayoutRootSelector : composerSelector], ); - partFixtures.input = makeDomNode("generic-input", partFixtures.composer, new Map(), [inputSelector]); + const inputParent = modernComposerLayout + ? (partFixtures.composerFooter = makeDomNode( + "generic-composer-footer", partFixtures.composer, new Map(), [composerSelector], + )) + : partFixtures.composer; + partFixtures.input = makeDomNode("generic-input", inputParent, new Map(), [inputSelector]); } partFixtures.unrelatedAside = makeDomNode( "generic-content-aside", partFixtures.main, new Map(), [sidebarSelector], @@ -168,8 +176,8 @@ function makeFixture({ register(messageSelector, partFixtures.userMessage); register(messageSelector, partFixtures.assistantMessage); } - register(".composer-surface-chrome", partFixtures.composer); - register('.composer-surface-chrome [class*="_footer_"]', partFixtures.composerToolbar); + register(':is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"])', partFixtures.composer); + register(':is(.composer-surface-chrome [class*="_footer_"], [class*="_ComposerLayoutRoot_"] [class*="_ComposerLayoutFooter_"])', partFixtures.composerToolbar); } const makeStyleNode = () => { const node = { @@ -231,6 +239,11 @@ function makeFixture({ const context = { window, document, + location: { + protocol: "app:", + pathname, + search: initialRoute ? `?initialRoute=${encodeURIComponent(initialRoute)}` : "", + }, MutationObserver: MockMutationObserver, CSSStyleSheet: adopted ? MockSheet : undefined, Blob, @@ -240,6 +253,7 @@ function makeFixture({ createObjectURL() { nextBlob += 1; return `blob:fixture-${nextBlob}`; }, revokeObjectURL(value) { revoked.push(value); }, }, + URLSearchParams, performance: { now: () => 1 }, setTimeout(callback, delay) { const id = ++nextId; timers.set(id, { callback, delay }); return id; }, clearTimeout(id) { timers.delete(id); }, @@ -346,7 +360,7 @@ export async function runRendererRuntimeTest(assetRoot) { assert.match(css, /background-image:\s*var\(--ds-task-full-veil\),\s*var\(--dream-skin-art\)/); assert.match( css, - /(?:__DREAM_SELECTOR_COMPOSER_CHROME__|\.composer-surface-chrome)\s*\{[^}]*background:\s*rgb\(var\(--ds-panel-rgb\) \/ \.94\)/, + /(?:__DREAM_SELECTOR_COMPOSER_CHROME__|:is\(\.composer-surface-chrome, \[class\*="_ComposerLayoutRoot_"\]\)|\.composer-surface-chrome)\s*\{[^}]*background:\s*rgb\(var\(--ds-panel-rgb\) \/ \.94\)/, "Accent foreground contrast must model the composer panel's 94% RGB surface", ); assert.match( @@ -426,6 +440,33 @@ export async function runRendererRuntimeTest(assetRoot) { assert.equal(home.partFixtures[fixtureKey].getAttribute("data-ds-part"), part, `${part} must be exposed through the public Safe CSS bridge`); } + + const petOverlay = makeFixture({ nativeAppearance: "dark", initialRoute: "/avatar-overlay" }); + vm.runInNewContext(petOverlay.payloadFor(), petOverlay.context); + assert.equal(petOverlay.window.__CODEX_DREAM_SKIN_STATE__, undefined, + "The avatar overlay must reject Dream Skin before installing renderer state."); + assert.equal(petOverlay.window.__CODEX_DREAM_SKIN_DISABLED__, true); + assert.equal(petOverlay.document.adoptedStyleSheets.length, 0); + assert.equal(petOverlay.attrs.get("data-dream-skin"), undefined); + + const petComposition = makeFixture({ + nativeAppearance: "dark", pathname: "/avatar-overlay-composition-surface.html", + }); + vm.runInNewContext(petComposition.payloadFor(), petComposition.context); + assert.equal(petComposition.window.__CODEX_DREAM_SKIN_STATE__, undefined, + "Pet composition surfaces must remain transparent and unthemed."); + assert.equal(petComposition.document.adoptedStyleSheets.length, 0); + + const navigatedPet = makeFixture({ nativeAppearance: "dark" }); + vm.runInNewContext(navigatedPet.payloadFor(), navigatedPet.context); + navigatedPet.context.location.pathname = "/avatar-overlay-composition-surface.html"; + vm.runInNewContext(navigatedPet.payloadFor(), navigatedPet.context); + assert.equal(navigatedPet.window.__CODEX_DREAM_SKIN_STATE__, undefined, + "Reapplying on a Pet route must clean an older renderer injection."); + assert.equal(navigatedPet.document.adoptedStyleSheets.length, 0); + assert.equal(navigatedPet.attrs.get("data-dream-skin"), undefined); + assert.equal(navigatedPet.revoked.length, 1, + "Pet cleanup must revoke the previous wallpaper blob URL."); const dynamicMessage = home.addDynamicMessage(); partObserver.callback([{ type: "childList" }]); home.flushTimers(80); @@ -454,6 +495,15 @@ export async function runRendererRuntimeTest(assetRoot) { assert.equal(generic.partFixtures.dialogInput.getAttribute("data-ds-part"), null, "Dialog inputs must not be mistaken for the app composer."); + const modernComposer = makeFixture({ + nativeAppearance: "dark", generic: true, modernComposerLayout: true, + }); + vm.runInNewContext(modernComposer.payloadFor(), modernComposer.context); + assert.equal(modernComposer.partFixtures.composer.getAttribute("data-ds-part"), "composer", + "The ComposerLayoutRoot wrapper must receive the public composer part."); + assert.equal(modernComposer.partFixtures.composerFooter.getAttribute("data-ds-part"), null, + "The broad composer fallback must not stop at ComposerLayoutFooter."); + const genericSearch = makeFixture({ nativeAppearance: "dark", generic: true, genericComposer: false, genericSearch: true, }); diff --git a/tools/selectors.json b/tools/selectors.json index 98347146..17e78a79 100644 --- a/tools/selectors.json +++ b/tools/selectors.json @@ -35,8 +35,8 @@ { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, - { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, - { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, + { "key": "composer-chrome", "selector": ":is(.composer-surface-chrome, [class*=\"_ComposerLayoutRoot_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。兼容旧语义类与新版 CSS Modules 根容器;hash 随版本变化。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, + { "key": "composer-toolbar", "selector": ":is(.composer-surface-chrome [class*=\"_footer_\"], [class*=\"_ComposerLayoutRoot_\"] [class*=\"_ComposerLayoutFooter_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;兼容旧 footer 前缀与新版 ComposerLayoutFooter CSS Module,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, diff --git a/windows/assets/dream-skin.css b/windows/assets/dream-skin.css index 939ce094..86dcfbf8 100644 --- a/windows/assets/dream-skin.css +++ b/windows/assets/dream-skin.css @@ -428,7 +428,7 @@ html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream- } html[data-dream-skin="active"]:is([data-dream-task-mode="ambient"], [data-dream-art-task-mode="ambient"], [data-dream-task-mode="banner"], [data-dream-art-task-mode="banner"])[data-dream-art-wide="true"]:has(main:is(.main-surface, [data-app-shell-main-surface], [class*="_MainContentSurface_"])):not(:has(main:is(.main-surface, [data-app-shell-main-surface], [class*="_MainContentSurface_"]) [role="main"])) - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; box-shadow: @@ -890,7 +890,7 @@ html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .gro html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:first-child { justify-content: center !important; } html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]) .group\/home-suggestions button > span:last-child { align-items: center !important; text-align: center !important; } -html[data-dream-skin="active"] .composer-surface-chrome { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { overflow: visible !important; border: 1px solid rgb(var(--ds-muted-rgb) / .18) !important; border-radius: 22px !important; @@ -899,7 +899,7 @@ html[data-dream-skin="active"] .composer-surface-chrome { backdrop-filter: blur(16px) saturate(108%) !important; } -html[data-dream-skin="active"] .composer-surface-chrome::before { content: none !important; } +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"])::before { content: none !important; } /* Newer app shells may no longer expose the selector contract above. The renderer assigns these parts only from one coherent main/composer/sidebar @@ -1082,7 +1082,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] main:is(.main-surface backdrop-filter: blur(8px) saturate(104%) !important; } -html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chrome { +html[data-dream-skin="active"][data-dream-art-wide="true"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { background: var(--ds-immersive-composer-solid) !important; border: 0 !important; box-shadow: @@ -1093,7 +1093,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] .composer-surface-chr } html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="true"] - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { box-shadow: 0 10px 28px rgb(var(--ds-bg-rgb) / .14), inset 0 0 0 1px var(--ds-immersive-line), @@ -1102,7 +1102,7 @@ html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="tr } html[data-dream-skin="active"][data-dream-shell="light"] - .composer-surface-chrome p.placeholder::after { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .78) !important; opacity: 1 !important; } @@ -1125,7 +1125,7 @@ html[data-dream-skin="active"] [class*="_homeUtilityBar_"] { } html[data-dream-skin="active"] [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) - .composer-surface-chrome { + :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { border: 0 !important; border-radius: 0 0 22px 22px !important; box-shadow: @@ -1145,7 +1145,7 @@ html[data-dream-skin="active"][data-dream-art-wide="true"] [class*="_homeUtility } html[data-dream-skin="active"][data-dream-art-wide="true"] - [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) .composer-surface-chrome { + [role="main"]:has([data-testid="home-icon"]):has([class*="_homeUtilityBar_"]) :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) { box-shadow: 0 10px 30px rgb(var(--ds-bg-rgb) / .20), inset 1px 0 var(--ds-immersive-line), @@ -1163,27 +1163,27 @@ html[data-dream-skin="active"][data-dream-shell="light"][data-dream-art-wide="tr } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]) { color: var(--ds-muted) !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button svg, -html[data-dream-skin="active"] .composer-surface-chrome button svg { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button svg { color: currentColor !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button *, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]) * { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]) * { color: currentColor !important; } html[data-dream-skin="active"] [class*="_homeUtilityBar_"] button:hover, -html[data-dream-skin="active"] .composer-surface-chrome button:not([class~="bg-token-foreground"]):hover { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) button:not([class~="bg-token-foreground"]):hover { color: var(--ds-text) !important; background: rgb(var(--ds-accent-rgb) / .10) !important; } -html[data-dream-skin="active"] .composer-surface-chrome p.placeholder::after { +html[data-dream-skin="active"] :is(.composer-surface-chrome, [class*="_ComposerLayoutRoot_"]) p.placeholder::after { color: rgb(var(--ds-muted-rgb) / .82) !important; opacity: 1 !important; } diff --git a/windows/assets/renderer-inject.js b/windows/assets/renderer-inject.js index 6076e7ab..45b02e56 100644 --- a/windows/assets/renderer-inject.js +++ b/windows/assets/renderer-inject.js @@ -1,6 +1,6 @@ // Canonical cross-platform renderer. Run tools/sync-runtime-assets.mjs after editing. ((cssText, artDataUrl, themeConfig) => { - const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":".composer-surface-chrome","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":".composer-surface-chrome [class*=\"_footer_\"]","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"settings-panel","selector":"[data-settings-panel-slug=\"general-settings\"]","tier":"L2","scope":"settings","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; + const SELECTOR_CONTRACT = {"schema":"codex-dream-skin-selectors/1","selectors":[{"key":"shell-main","selector":"main:is(.main-surface, [data-app-shell-main-surface], [class*=\"_MainContentSurface_\"])","tier":"L1","scope":"all","required":true},{"key":"left-panel","selector":"aside.app-shell-left-panel","tier":"L1","scope":"all","required":true},{"key":"header-tint","selector":"header:is(.app-header-tint, [data-app-shell-header-edge-scroll], [class*=\"_Header_\"])","tier":"L1","scope":"all","required":true},{"key":"main-content-top-fade","selector":":is(.app-shell-main-content-top-fade, [data-app-shell-main-content-top-fade], [class*=\"_MainContentTopFade_\"])","tier":"L2","scope":"all","required":false},{"key":"home-icon","selector":"[data-testid=\"home-icon\"]","tier":"L1","scope":"home","required":true},{"key":"home-route","selector":"[role=\"main\"]:has([data-testid=\"home-icon\"])","tier":"L1","scope":"home","required":true},{"key":"home-route-css","selector":"[role=\"main\"]","tier":"L1","scope":"home","required":true},{"key":"home-banners","selector":".home-banners","tier":"L2","scope":"home","required":false},{"key":"composer-chrome","selector":":is(.composer-surface-chrome, [class*=\"_ComposerLayoutRoot_\"])","tier":"L2","scope":"home+thread","required":false},{"key":"composer-toolbar","selector":":is(.composer-surface-chrome [class*=\"_footer_\"], [class*=\"_ComposerLayoutRoot_\"] [class*=\"_ComposerLayoutFooter_\"])","tier":"L2","scope":"home+thread","required":false},{"key":"home-utility","selector":"[class*=\"_homeUtilityBar_\"]","tier":"L2","scope":"home","required":false},{"key":"game-source","selector":"[data-feature=\"game-source\"]","tier":"L2","scope":"home","required":false},{"key":"home-suggestions","selector":".group\\/home-suggestions","tier":"L2","scope":"home","required":false},{"key":"project-selector","selector":".group\\/project-selector","tier":"L2","scope":"home config","required":false},{"key":"markdown","selector":"[class*=\"_markdown\"]","tier":"L2","scope":"thread","required":false},{"key":"thread-surface","selector":".thread-scroll-container","tier":"L2","scope":"thread","required":false},{"key":"message","selector":":is([data-message-author-role], [data-local-conversation-user-anchor], [data-local-conversation-final-assistant])","tier":"L2","scope":"thread","required":false},{"key":"settings-panel","selector":"[data-settings-panel-slug=\"general-settings\"]","tier":"L2","scope":"settings","required":false},{"key":"appearance-radio","selector":"input[name=\"appearance-theme\"]","tier":"L2","scope":"settings","required":false},{"key":"overlay-menu","selector":"[role=\"menu\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-dialog","selector":"[role=\"dialog\"]","tier":"L2","scope":"overlay","required":false},{"key":"overlay-popper","selector":"[data-radix-popper-content-wrapper]","tier":"L2","scope":"overlay","required":false}],"stableTestids":["app-shell-header-context-menu-surface","home-icon","theme-preview"]}; const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__"; const DISABLED_KEY = "__CODEX_DREAM_SKIN_DISABLED__"; const STYLE_REGISTRY_KEY = "__CODEX_DREAM_SKIN_STYLE_SHEETS__"; @@ -13,6 +13,19 @@ "data-dream-art-safe-area", "data-dream-art-task-mode", "data-dream-art-aspect", "data-dream-art-ready", ]; + const initialRoute = new URLSearchParams(String(location.search || "")) + .get("initialRoute") || ""; + const pathname = String(location.pathname || ""); + const excludedPetSurface = location.protocol === "app:" && ( + pathname.endsWith("/avatar-overlay-composition-surface.html") || + initialRoute === "/avatar-overlay" || initialRoute.startsWith("/avatar-overlay/") + ); + if (excludedPetSurface) { + const previous = window[STATE_KEY]; + if (typeof previous?.cleanup === "function") previous.cleanup(); + window[DISABLED_KEY] = true; + return; + } const VERSION = __DREAM_SKIN_VERSION_JSON__; const STYLE_REVISION = __DREAM_SKIN_STYLE_REVISION_JSON__; const PAYLOAD_REVISION = __DREAM_SKIN_PAYLOAD_REVISION_JSON__; @@ -670,6 +683,8 @@ const main = resolvedMainNode(); for (const input of genericInputNodes()) { if (main && !main.contains?.(input)) continue; + const layoutRoot = input.closest?.('[class*="_ComposerLayoutRoot_"]'); + if (layoutRoot && (!main || main.contains?.(layoutRoot))) return [layoutRoot]; const owner = input.closest?.( '[data-testid*="composer" i], [data-testid*="prompt" i], ' + '[class*="composer" i], [class*="prompt" i]', diff --git a/windows/assets/selectors.json b/windows/assets/selectors.json index 98347146..17e78a79 100644 --- a/windows/assets/selectors.json +++ b/windows/assets/selectors.json @@ -35,8 +35,8 @@ { "key": "home-route", "selector": "[role=\"main\"]:has([data-testid=\"home-icon\"])", "tier": "L1", "scope": "home", "required": true, "notes": "首页容器。实测 [role=main] 仅存在于首页——它不是通用路由锚点,thread 页没有" }, { "key": "home-route-css", "selector": "[role=\"main\"]", "tier": "L1", "scope": "home", "required": true, "notes": "home-route 的 CSS 可组合别名:不含 :has()。CSS 禁止 :has() 嵌套,home-route 原选择器一旦写进 :has()/:not(:has()) 整条规则会被解析器丢弃(v1.3.1 全窗口首页与任务页氛围背景因此失效)。依据与 home-route 相同的实测:[role=main] 仅存在于首页;该锚点缺失时 CSS 路由门控整体失效,故必须按 L1 必需项监控" }, { "key": "home-banners", "selector": ".home-banners", "tier": "L2", "scope": "home", "required": false, "notes": "Codex 26.721+ 新增的原生首页 banner 插槽,位于 home-route 第一个子节点内、通常为空(0 子节点)靠原生 empty:hidden 折叠。真正的首页内容(标题/建议卡/输入框)在这个版本上已变成该插槽外层容器的兄弟节点,不再是它的后代(见 #244 实测:home-route > div:first-child 被撑到 100% 高度会把内容挤出视口)。用于判定新旧 home DOM 形态,皮肤据此把 first-child 链路的强制尺寸限定在旧形态或改走兄弟节点" }, - { "key": "composer-chrome", "selector": ".composer-surface-chrome", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, - { "key": "composer-toolbar", "selector": ".composer-surface-chrome [class*=\"_footer_\"]", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;CSS Modules 名称跨端稳定,hash 随版本变化" }, + { "key": "composer-chrome", "selector": ":is(.composer-surface-chrome, [class*=\"_ComposerLayoutRoot_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框壳。兼容旧语义类与新版 CSS Modules 根容器;hash 随版本变化。win 实测存在无 composer 的 detail 路由,不可当全局锚点" }, + { "key": "composer-toolbar", "selector": ":is(.composer-surface-chrome [class*=\"_footer_\"], [class*=\"_ComposerLayoutRoot_\"] [class*=\"_ComposerLayoutFooter_\"])", "tier": "L2", "scope": "home+thread", "required": false, "notes": "输入框工具栏;兼容旧 footer 前缀与新版 ComposerLayoutFooter CSS Module,hash 随版本变化" }, { "key": "home-utility", "selector": "[class*=\"_homeUtilityBar_\"]", "tier": "L2", "scope": "home", "required": false, "notes": "CSS Modules 前缀匹配;模块名跨端稳定、hash 随版本变,禁止匹配完整 hash" }, { "key": "game-source", "selector": "[data-feature=\"game-source\"]", "tier": "L2", "scope": "home", "required": false }, { "key": "home-suggestions", "selector": ".group\\/home-suggestions", "tier": "L2", "scope": "home", "required": false }, diff --git a/windows/scripts/injector.mjs b/windows/scripts/injector.mjs index b23a1ad4..cd2c9caa 100644 --- a/windows/scripts/injector.mjs +++ b/windows/scripts/injector.mjs @@ -707,6 +707,13 @@ async function readThemeSourceStamp(loadedTheme) { async function probeSession(session) { return session.evaluate(`(() => { + const initialRoute = new URLSearchParams(String(location.search || '')) + .get('initialRoute') || ''; + const pathname = String(location.pathname || ''); + const excludedPetSurface = location.protocol === 'app:' && ( + pathname.endsWith('/avatar-overlay-composition-surface.html') || + initialRoute === '/avatar-overlay' || initialRoute.startsWith('/avatar-overlay/') + ); const genericCodexSurface = () => { if (location.protocol !== 'app:') return false; const main = document.querySelector('main, [role="main"]'); @@ -728,7 +735,8 @@ async function probeSession(session) { Boolean(document.querySelector(${stableTestidLiteral("theme-preview")})); return { markers, - codex: location.protocol === 'app:' && + excludedPetSurface, + codex: !excludedPetSurface && location.protocol === 'app:' && ((markers.shell && markers.sidebar) || settings || markers.main || markers.generic), }; })()`); @@ -835,7 +843,12 @@ async function connectCodexTargets(port, timeoutMs, expectedBrowserId) { session = await connectTarget(target, port); const probe = await probeSession(session); if (probe?.codex) connected.push({ target, session, probe }); - else session.close(); + else { + if (probe?.excludedPetSurface && !await cleanupExcludedSurface(session)) { + throw new Error("Excluded Pet surface cleanup did not verify"); + } + session.close(); + } } catch (error) { session?.close(); lastError = error; @@ -1136,6 +1149,11 @@ async function verifyRemovedSession(session) { })()`); } +export async function cleanupExcludedSurface(session) { + if (!await removeFromSession(session)) return false; + return verifyRemovedSession(session); +} + export async function verifySession( session, targetId, @@ -1689,6 +1707,9 @@ async function runWatch(options) { const probe = await waitForCodexProbe(session); if (!probe?.codex) { await removeEarlyPayload(session, earlyScriptId); + if (probe?.excludedPetSurface && !await cleanupExcludedSurface(session)) { + throw new Error("Excluded Pet surface cleanup did not verify"); + } rejectTarget(target, 5000); session.close(); continue; diff --git a/windows/tests/injector-bootstrap.test.mjs b/windows/tests/injector-bootstrap.test.mjs index 9353fb0d..dec2ed63 100644 --- a/windows/tests/injector-bootstrap.test.mjs +++ b/windows/tests/injector-bootstrap.test.mjs @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import vm from "node:vm"; import { fileURLToPath } from "node:url"; -import { earlyPayloadFor } from "../scripts/injector.mjs"; +import { cleanupExcludedSurface, earlyPayloadFor } from "../scripts/injector.mjs"; const here = path.dirname(fileURLToPath(import.meta.url)); const injectorPath = path.resolve(here, "../scripts/injector.mjs"); @@ -149,8 +149,14 @@ const liveProbePayload = vm.runInNewContext(`\`${probeTemplate}\``, { const runLiveProbe = ({ protocol = "app:", settingsPanel: hasSettingsPanel = false, genericMain = false, genericInput = false, branding = false, + pathname = "/index.html", initialRoute = "", } = {}) => vm.runInNewContext(liveProbePayload, { - location: { protocol }, + location: { + protocol, + pathname, + search: initialRoute ? `?initialRoute=${encodeURIComponent(initialRoute)}` : "", + }, + URLSearchParams, document: { querySelector(selector) { if (selector === "[selector-settings-panel]") return hasSettingsPanel ? {} : null; @@ -173,6 +179,25 @@ assert.equal(runLiveProbe({ genericMain: true, genericInput: true }).codex, fals "The live probe must reject an unbranded generic app target."); assert.equal(runLiveProbe({ genericMain: true, genericInput: true, branding: true }).codex, true, "The live probe may accept generic anchors only with the stable Codex branding marker."); +const avatarOverlayProbe = runLiveProbe({ settingsPanel: true, initialRoute: "/avatar-overlay" }); +assert.equal(avatarOverlayProbe.excludedPetSurface, true); +assert.equal(avatarOverlayProbe.codex, false, + "The avatar overlay must never be treated as the primary Codex renderer."); +const petCompositionProbe = runLiveProbe({ + settingsPanel: true, pathname: "/avatar-overlay-composition-surface.html", +}); +assert.equal(petCompositionProbe.excludedPetSurface, true); +assert.equal(petCompositionProbe.codex, false, + "Pet composition surfaces must stay outside the Dream Skin target set."); +const cleanupEvaluations = []; +assert.equal(await cleanupExcludedSurface({ + async evaluate(expression) { cleanupEvaluations.push(expression); return true; }, +}), true, "Excluded Pet cleanup must remove and verify stale renderer state."); +assert.equal(cleanupEvaluations.length, 2); +assert.match(cleanupEvaluations[0], /__CODEX_DREAM_SKIN_DISABLED__/); +assert.match(cleanupEvaluations[1], /hasAttributes/); +assert.ok((source.match(/probe\?\.excludedPetSurface && !await cleanupExcludedSurface/g) || []).length >= 2, + "One-shot and watcher discovery must both clean excluded Pet targets."); assert.match(identityProbeSource, /selectorLiteral\("settings-panel"\)/, "The live probe must retain the current Settings structural marker."); assert.match(identityProbeSource, /return Boolean\(main && input && branded\)/, From c1433be12252b868b4a0cabc338ed03afdf04a9f Mon Sep 17 00:00:00 2001 From: huangjinnan_mac Date: Tue, 18 Aug 2026 20:49:15 +0800 Subject: [PATCH 2/5] docs(changelog): record renderer surface fixes Document the cross-platform composer targeting and Pet overlay cleanup fixes under the unreleased sections without changing client versions. --- macos/CHANGELOG.md | 1 + windows/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/macos/CHANGELOG.md b/macos/CHANGELOG.md index b9805db8..f427fa43 100644 --- a/macos/CHANGELOG.md +++ b/macos/CHANGELOG.md @@ -10,6 +10,7 @@ ### 修复 +- 修复新版 Codex 把输入框壳迁移到 `_ComposerLayoutRoot_` 后,Dream Skin 误把 `_ComposerLayoutFooter_` 标记为 composer、导致主题输入框样式只落在底部工具栏的问题;同时排除 `/avatar-overlay` 与 Pet composition surface,并在发现旧注入时执行移除与验证,避免主题壁纸污染透明 Pet 窗口形成矩形背景。 - 修复透明或极端显式强调色下按钮文字对比度不稳定的问题:共享渲染器现在按真实 composer 面板表面和最坏背景计算前景色,并正确处理 alpha 与 RGB 夹取(#351)。 - 修复菜单栏「重新应用皮肤」与「打开 ChatGPT」入口语义不一致的问题(#341):重新应用现在会先尝试热重载,失败才回退到完整重启;打开 ChatGPT 复用已安装的 Dream Skin 启动脚本而不是直接原生拉起。 - 渲染器可见性校验加固(#294):从仅检查宽高与 `display` 改为同时校验 `checkVisibility()`、`opacity`、`content-visibility` 与视口相交,减少"校验通过但皮肤实际不可见"的假阳性。已核实社区主题的 Safe CSS 沙盒不允许 `opacity` 低于 0.65、且不开放 `display`/`visibility`/`position`,不会被这次加固误伤。 diff --git a/windows/CHANGELOG.md b/windows/CHANGELOG.md index aa323ae9..6ca2f20c 100644 --- a/windows/CHANGELOG.md +++ b/windows/CHANGELOG.md @@ -8,6 +8,7 @@ ### 修复 +- 修复新版 Codex 把输入框壳迁移到 `_ComposerLayoutRoot_` 后,Dream Skin 误把 `_ComposerLayoutFooter_` 标记为 composer、导致主题输入框样式只落在底部工具栏的问题;同时排除 `/avatar-overlay` 与 Pet composition surface,并在发现旧注入时执行移除与验证,避免主题壁纸污染透明 Pet 窗口形成矩形背景。 - 修复 Windows 首次或冷启动状态下从 DreamSkin.cc 一键换肤被错误拒绝的问题。客户端现在会在下载主题包前安全启动并验证当前主题;如果官方 Codex 未提供可用 CDP 端点,则在任何主题库或活动主题写入前返回明确失败。 - 修复透明或极端显式强调色下按钮文字对比度不稳定的问题:共享渲染器现在按真实 composer 面板表面和最坏背景计算前景色,并正确处理 alpha 与 RGB 夹取(#351)。 - 修复 Windows 启动失败后只留下颜色、背景等部分外观状态的问题(#354、#357):新增有界持久 journal 和三方恢复,保留较新的用户编辑;一键换肤在慢启动、超时或渲染未确认时不强杀仍运行的子进程,也不留下混合的活动主题文件,并提供可区分的有界失败原因。该修复处理本地失败启动的状态一致性,不改变官方 Codex 的 CDP 能力。 From ad155e87837f5decf7701ee49ab4a793726379a2 Mon Sep 17 00:00:00 2001 From: huangjinnan_mac Date: Tue, 18 Aug 2026 20:53:01 +0800 Subject: [PATCH 3/5] chore(progress): record pushed renderer fix Capture the verified commits, fork branch, prepared PR evidence, and the temporary locked-session blocker for the final GitHub submission. --- TASK_PROGRESS.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index 0c3c82a8..fd38b006 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -44,12 +44,17 @@ Medium, or Low issues. It confirmed renderer self-exclusion, verified cleanup in one-shot and watcher paths, ComposerLayoutRoot selection, generated asset synchronization, and focused dual-platform coverage. -- [current] Implementation, generated assets, tests, and review are complete. - No commit, push, or PR yet. Fork `hjnnjh/Codex-Dream-Skin` now exists for the - contribution branch. -- [next] Inspect and stage the final diff, create an atomic commit, push the - fork branch, and open a Ready PR with exact test/gap evidence. Native Windows - PowerShell 5.1/7 remains a CI gate. +- [committed/pushed] Implementation and test commit + `c4c3a7e90c0def8c4a6961ca013ec2ac36ca15b8` plus changelog commit + `c1433be12252b868b4a0cabc338ed03afdf04a9f` are pushed to + `hjnnjh/Codex-Dream-Skin:codex/fix-composer-pet-scope`. The public compare is + 3 commits ahead of upstream after this progress checkpoint and 0 behind. +- [blocked] PR title/body and template evidence are prepared, but the final + GitHub web submission is blocked because the macOS session locked while + Safari held the authenticated GitHub state. No PR exists yet. +- [next] Unlock the Mac, open a Ready PR against `Fei-Away/main`, then record + the PR URL and require exact-head CI. Native Windows PowerShell 5.1/7 remains + a CI gate. ## Issue #352 fix and v1.5.14 release (2026-08-12) From c8ae63bcf1f7f2cc48113fe84cffb36cc31256b9 Mon Sep 17 00:00:00 2001 From: huangjinnan_mac Date: Tue, 18 Aug 2026 21:14:27 +0800 Subject: [PATCH 4/5] chore(progress): record pull request Record Ready PR #372 and the remaining exact-head CI gates without claiming merge or release status. --- TASK_PROGRESS.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index fd38b006..8fbe0930 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -49,12 +49,13 @@ `c1433be12252b868b4a0cabc338ed03afdf04a9f` are pushed to `hjnnjh/Codex-Dream-Skin:codex/fix-composer-pet-scope`. The public compare is 3 commits ahead of upstream after this progress checkpoint and 0 behind. -- [blocked] PR title/body and template evidence are prepared, but the final - GitHub web submission is blocked because the macOS session locked while - Safari held the authenticated GitHub state. No PR exists yet. -- [next] Unlock the Mac, open a Ready PR against `Fei-Away/main`, then record - the PR URL and require exact-head CI. Native Windows PowerShell 5.1/7 remains - a CI gate. +- [PR open] Ready PR #372 targets `Fei-Away/main`: + `https://github.com/Fei-Away/Codex-Dream-Skin/pull/372`. The implementation, + changelog, and first progress checkpoint are pushed; the PR is not merged or + released. +- [next] Push this PR-status checkpoint, then require exact-head Static, + macOS/DMG, Windows PowerShell 7, and Windows PowerShell 5.1/Setup CI before + any merge decision. Native Windows verification remains a CI gate. ## Issue #352 fix and v1.5.14 release (2026-08-12) From d2c01e6ec3f74c21963bb734989be14619c729a5 Mon Sep 17 00:00:00 2001 From: huangjinnan_mac Date: Wed, 19 Aug 2026 10:11:17 +0800 Subject: [PATCH 5/5] docs(progress): remove host-specific path Address PR review by removing the contributor machine's absolute clone path from the durable progress record. --- TASK_PROGRESS.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index 8fbe0930..f6debf1e 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -6,9 +6,8 @@ `_ComposerLayoutFooter_` instead of `_ComposerLayoutRoot_`, and prevent Dream Skin from injecting wallpaper/CSS into `/avatar-overlay` and Pet composition surfaces that must remain transparent. -- [scope] Isolated clone - `/Users/huangjinnan/Desktop/Codex-Dream-Skin-pr-composer-pet-fix`, branch - `codex/fix-composer-pet-scope`, based on exact `origin/main@95423d8` (v1.5.14). +- [scope] Isolated branch `codex/fix-composer-pet-scope`, based on exact + `origin/main@95423d8` (v1.5.14). Shared runtime source, generated dual-platform assets, focused regressions, and this progress record are in scope. User-specific theme colors are not. - [field evidence] Live macOS Codex exposed `_ComposerLayoutRoot_*` around the @@ -44,6 +43,8 @@ Medium, or Low issues. It confirmed renderer self-exclusion, verified cleanup in one-shot and watcher paths, ComposerLayoutRoot selection, generated asset synchronization, and focused dual-platform coverage. +- [review response] Removed the host-specific absolute clone path from this + progress record after PR review; no product code changes were requested. - [committed/pushed] Implementation and test commit `c4c3a7e90c0def8c4a6961ca013ec2ac36ca15b8` plus changelog commit `c1433be12252b868b4a0cabc338ed03afdf04a9f` are pushed to