diff --git a/TASK_PROGRESS.md b/TASK_PROGRESS.md index 9f538f60..f6debf1e 100644 --- a/TASK_PROGRESS.md +++ b/TASK_PROGRESS.md @@ -1,5 +1,63 @@ # 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 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. +- [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 + `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. +- [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) - [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/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/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/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 能力。 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\)/,