From 39f60a3e88b2a5ce06d6cb5eda2e68e361d6b81f Mon Sep 17 00:00:00 2001 From: guicheng Date: Sat, 29 Aug 2026 15:55:33 +0800 Subject: [PATCH 1/7] fix: unify modern Codex Dream Skin thread surface --- .../codex-plus-manager/src/dream-skin.test.ts | 26 ++- .../src/renderer-inject.test.ts | 10 ++ assets/inject/renderer-inject.js | 163 +++++++++++++++++- .../cidala-tiger/macos/renderer-inject.js | 9 +- .../dream-skin/macos/renderer-inject.js | 9 +- crates/codex-plus-core/src/assets.rs | 136 +++++++++++++-- .../codex-plus-core/src/dream_skin_library.rs | 31 ++++ .../codex-plus-core/src/dream_skin_runtime.rs | 8 +- crates/codex-plus-core/tests/cdp_bridge.rs | 5 +- .../tests/dream_skin_package.rs | 24 ++- .../tests/dream_skin_runtime.rs | 35 +++- 11 files changed, 421 insertions(+), 35 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index 69bcc754e..f1e17f326 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -99,7 +99,8 @@ describe("dream skin theme helpers", () => { assert.match(renderer, /\.composer-surface-chrome/); assert.match(renderer, /data:image\/(?:png|jpeg|webp|gif);base64/); assert.match(renderer, /removeDreamSkinCompanion/); - assert.match(renderer, /ensureDreamSkinCompanion\(\s*window\.__CODEX_PLUS_DREAM_SKIN_THEME__/); + assert.match(renderer, /const theme = window\.__CODEX_PLUS_DREAM_SKIN_THEME__/); + assert.match(renderer, /ensureDreamSkinCompanion\(theme\)/); }); it("aligns tall companion images by rendered height with a wider vertical offset range", async () => { @@ -138,7 +139,28 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "20-modern-main-surface"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "23-modern-codex-skin-api-compat"/); + }); + + it("bridges the modern Codex Skin API without forcing visual properties", async () => { + const assets = await readFile( + new URL("../../../crates/codex-plus-core/src/assets.rs", import.meta.url), + "utf8", + ); + + assert.match(assets, /--ds-theme-font-family/); + assert.match(assets, /--ds-theme-surface-radius/); + assert.match(assets, /--ds-theme-surface-opacity/); + assert.match(assets, /--ds-theme-surface-blur/); + assert.match(assets, /dream-skin\/themes/); + assert.match(assets, /join\("theme\.css"\)/); + assert.match(assets, /threadViewport/); + assert.match(assets, /const threadSurface = threadViewport \|\| threadScroll/); + assert.match(assets, /data-ds-thread-scroll/); + assert.match(assets, /data-app-shell-main-surface/); + assert.match(assets, /data-local-conversation-user-anchor/); + assert.match(assets, /_ComposerLayoutRoot_/); + assert.doesNotMatch(assets, /DREAM_MODERN_CODEX_COMPAT_CSS/); }); it("extends the Windows wallpaper treatment to right and bottom dock panels", async () => { diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index 1b756840f..7c0275730 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -255,6 +255,16 @@ describe("renderer injection header compatibility", () => { assert.match(renderer, /ensureShellMain/); } }); + + it("keeps modern Dream Skin composers out of reverse thread scrolling", async () => { + const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8"); + + assert.match(renderer, /data-thread-scroll-footer/); + assert.match(renderer, /_ComposerLayoutRoot_/); + assert.match(renderer, /syncDreamSkinComposerDocking/); + assert.match(renderer, /data-codex-plus-dreamskin-composer-docked/); + assert.match(renderer, /footer\.style\.setProperty\(property, value, "important"\)/); + }); }); /** 从注入脚本里取出 `shouldScheduleScan`,配上可控的依赖来跑。 */ diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 9a2ac85f8..1a67b936b 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -1551,7 +1551,9 @@ const existing = document.querySelector("main.main-surface"); if (existing) return existing; - const modularSurface = document.querySelector('main[class*="_MainContentSurface_"]'); + const modularSurface = document.querySelector( + 'main[data-app-shell-main-surface], main[class*="MainContentSurface"], main[class*="_MainContentSurface_"]', + ); const mainCandidates = modularSurface ? [] : [...document.querySelectorAll("main")]; const shellMain = modularSurface || (mainCandidates.length === 1 ? mainCandidates[0] : null); if (!shellMain) return null; @@ -1769,12 +1771,114 @@ } function visibleDreamSkinComposer() { - return [...document.querySelectorAll(".composer-footer, .composer-surface-chrome")] + return [...document.querySelectorAll( + ".composer-footer, .composer-surface-chrome, [data-thread-scroll-footer], [class*='_ComposerLayoutRoot_'], [data-composer-surface-variant][data-composer-radius-variant]", + )] .map((node) => ({ node, rect: node.getBoundingClientRect?.() })) .filter(({ rect }) => rect && rect.width > 200 && rect.height > 0) .sort((left, right) => right.rect.bottom - left.rect.bottom)[0] || null; } + const codexPlusDreamSkinComposerDockAttribute = "data-codex-plus-dreamskin-composer-docked"; + const codexPlusDreamSkinComposerDockProperties = [ + "position", "top", "right", "bottom", "left", "width", "z-index", + ]; + const codexPlusDreamSkinComposerDockState = { + node: null, + scroll: null, + parent: null, + nextSibling: null, + attribute: null, + styles: null, + }; + + function restoreDreamSkinComposerDock() { + const state = codexPlusDreamSkinComposerDockState; + const composer = state.node; + if (!composer) return; + + if (composer.isConnected && state.parent?.isConnected) { + if (state.nextSibling?.parentElement === state.parent) state.parent.insertBefore(composer, state.nextSibling); + else state.parent.appendChild(composer); + } else if (composer.isConnected) { + // The old route may already have been unmounted. Do not leave an + // interactive composer from a previous conversation on the new route. + composer.remove(); + } + + if (composer.isConnected) { + for (const property of codexPlusDreamSkinComposerDockProperties) { + const saved = state.styles?.[property]; + if (saved?.value) composer.style.setProperty(property, saved.value, saved.priority || ""); + else composer.style.removeProperty(property); + } + if (state.attribute === null) composer.removeAttribute(codexPlusDreamSkinComposerDockAttribute); + else composer.setAttribute(codexPlusDreamSkinComposerDockAttribute, state.attribute); + } + + Object.assign(state, { + node: null, + scroll: null, + parent: null, + nextSibling: null, + attribute: null, + styles: null, + }); + } + + function syncDreamSkinComposerDocking(enabled) { + const state = codexPlusDreamSkinComposerDockState; + const main = document.querySelector( + "main.main-surface, main[data-app-shell-main-surface], main[class*='_MainContentSurface_']", + ); + const scroll = enabled + ? main?.querySelector(".thread-scroll-container[data-app-action-timeline-scroll], .thread-scroll-container") + : null; + const renderedFooter = scroll?.querySelector("[data-thread-scroll-footer]") || null; + + if (state.node && (state.scroll !== scroll || (renderedFooter && renderedFooter !== state.node))) { + restoreDreamSkinComposerDock(); + } + if (!enabled || !main || !scroll) { + if (state.node) restoreDreamSkinComposerDock(); + return; + } + + const footer = renderedFooter || (state.scroll === scroll && state.node?.isConnected ? state.node : null); + if (!footer) { + if (state.node) restoreDreamSkinComposerDock(); + return; + } + if (!state.node) { + // Codex may eventually own this layout itself, or a user script may + // already have moved the live node. Leave an already docked footer alone. + if (footer.parentElement === main) return; + state.node = footer; + state.scroll = scroll; + state.parent = footer.parentElement; + state.nextSibling = footer.nextSibling; + state.attribute = footer.getAttribute(codexPlusDreamSkinComposerDockAttribute); + state.styles = Object.fromEntries(codexPlusDreamSkinComposerDockProperties.map((property) => [ + property, + { value: footer.style.getPropertyValue(property), priority: footer.style.getPropertyPriority(property) }, + ])); + } + + if (footer.parentElement !== main) main.appendChild(footer); + footer.setAttribute(codexPlusDreamSkinComposerDockAttribute, "true"); + for (const [property, value] of Object.entries({ + position: "absolute", + top: "auto", + right: "0", + bottom: "0", + left: "0", + width: "auto", + "z-index": "10", + })) { + footer.style.setProperty(property, value, "important"); + } + } + function ensureDreamSkinCompanion(theme) { const config = dreamSkinCompanionConfig(theme); const composer = visibleDreamSkinComposer(); @@ -1842,6 +1946,7 @@ } function clearDreamSkinPresentation() { + restoreDreamSkinComposerDock(); const root = document.documentElement; for (const className of [...(root?.classList || [])]) { if ( @@ -1867,6 +1972,30 @@ "--ds-text", "--ds-muted", "--ds-line", + "--ds-theme-font-family", + "--ds-theme-font-scale", + "--ds-theme-surface-radius", + "--ds-theme-surface-opacity", + "--ds-theme-surface-blur", + "--ds-theme-surface-border-alpha", + "--ds-theme-surface-shadow", + "--ds-theme-image-focus-x", + "--ds-theme-image-focus-y", + "--ds-theme-image-zoom", + "--ds-theme-image-dim", + "--ds-theme-image-task-intensity", + "--ds-theme-density-scale", + "--ds-theme-motion-level", + "--ds-theme-color-background", + "--ds-theme-color-panel", + "--ds-theme-color-panel-alt", + "--ds-theme-color-accent", + "--ds-theme-color-accent-alt", + "--ds-theme-color-secondary", + "--ds-theme-color-highlight", + "--ds-theme-color-text", + "--ds-theme-color-muted", + "--ds-theme-color-line", "--dream-ink", "--dream-purple", "--dream-violet", @@ -1879,6 +2008,19 @@ "--dream-skin-project-prefix", "--dream-skin-project-label", ].forEach((name) => root?.style.removeProperty(name)); + window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__?.disconnect?.(); + delete window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__; + const registeredParts = new Set([ + "root", "sidebar", "main", "header", "home", "home-hero", "project-list", + "thread", "message", "composer", "composer-toolbar", "dialog", + ]); + document.querySelectorAll("[data-ds-part]").forEach((node) => { + if (registeredParts.has(node.getAttribute("data-ds-part"))) node.removeAttribute("data-ds-part"); + }); + document.querySelectorAll("[data-ds-thread-surface], [data-ds-thread-scroll]").forEach((node) => { + node.removeAttribute("data-ds-thread-surface"); + node.removeAttribute("data-ds-thread-scroll"); + }); document.querySelectorAll(".dream-home").forEach((node) => node.classList.remove("dream-home")); document.querySelectorAll('[role="main"][data-dream-home-layout]').forEach((node) => { node.removeAttribute("data-dream-home-layout"); @@ -2175,24 +2317,29 @@ function refreshDreamSkin() { const settings = codexPlusSettings(); - if (settings.dreamSkinEnabled && !settings.dreamSkinPaused) ensureDreamSkinMainSurface(); + const active = settings.dreamSkinEnabled && !settings.dreamSkinPaused; + if (active) ensureDreamSkinMainSurface(); if (window.__CODEX_PLUS_EXTERNAL_DREAM_SKIN_RUNTIME__) { - if (codexPlusBackendSettingsLoaded && (!settings.dreamSkinEnabled || settings.dreamSkinPaused)) { + if (codexPlusBackendSettingsLoaded && !active) { + syncDreamSkinComposerDocking(false); cleanupDreamSkin(); } else { + const theme = window.__CODEX_PLUS_DREAM_SKIN_THEME__ || settings.dreamSkinThemeConfig; const state = window.__CODEX_DREAM_SKIN_STATE__ || window.__CODEX_GLASS_VISION_SKIN_STATE__; state?.ensure?.(); - ensureDreamSkinCompanion( - window.__CODEX_PLUS_DREAM_SKIN_THEME__ || settings.dreamSkinThemeConfig, - ); + if (active) syncDreamSkinComposerDocking(true); + ensureDreamSkinCompanion(theme); } return; } - if (!settings.dreamSkinEnabled || settings.dreamSkinPaused) { + if (!active) { + syncDreamSkinComposerDocking(false); cleanupDreamSkin(); return; } installDreamSkin(settings); + syncDreamSkinComposerDocking(true); + ensureDreamSkinCompanion(settings.dreamSkinThemeConfig); } function applyDreamSkinLiveUpdate(payload) { diff --git a/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js b/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js index 79fa33a7c..8a3c70929 100644 --- a/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js +++ b/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js @@ -30,7 +30,14 @@ "--dream-skin-focus-x", "--dream-skin-focus-y", "--dream-skin-art-position", "--dream-skin-decor-intensity", "--dream-skin-name", "--dream-skin-tagline", "--dream-skin-project-prefix", - "--dream-skin-project-label", + "--dream-skin-project-label", "--ds-theme-color-background", "--ds-theme-color-panel", + "--ds-theme-color-panel-alt", "--ds-theme-color-accent", "--ds-theme-color-accent-alt", + "--ds-theme-color-secondary", "--ds-theme-color-highlight", "--ds-theme-color-text", + "--ds-theme-color-muted", "--ds-theme-color-line", "--ds-theme-font-family", "--ds-theme-font-scale", + "--ds-theme-surface-radius", "--ds-theme-surface-opacity", "--ds-theme-surface-blur", + "--ds-theme-surface-border-alpha", "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", + "--ds-theme-image-focus-y", "--ds-theme-image-zoom", "--ds-theme-image-dim", + "--ds-theme-image-task-intensity", "--ds-theme-density-scale", "--ds-theme-motion-level", ]; const installToken = {}; const existingAnalysisCache = window[ANALYSIS_CACHE_KEY]; diff --git a/assets/inject/upstream/dream-skin/macos/renderer-inject.js b/assets/inject/upstream/dream-skin/macos/renderer-inject.js index cd7264208..d3c2c4535 100644 --- a/assets/inject/upstream/dream-skin/macos/renderer-inject.js +++ b/assets/inject/upstream/dream-skin/macos/renderer-inject.js @@ -26,7 +26,14 @@ "--dream-art-focus-x", "--dream-art-focus-y", "--dream-art-position", "--dream-skin-focus-x", "--dream-skin-focus-y", "--dream-skin-art-position", "--dream-skin-name", "--dream-skin-tagline", "--dream-skin-project-prefix", - "--dream-skin-project-label", + "--dream-skin-project-label", "--ds-theme-color-background", "--ds-theme-color-panel", + "--ds-theme-color-panel-alt", "--ds-theme-color-accent", "--ds-theme-color-accent-alt", + "--ds-theme-color-secondary", "--ds-theme-color-highlight", "--ds-theme-color-text", + "--ds-theme-color-muted", "--ds-theme-color-line", "--ds-theme-font-family", "--ds-theme-font-scale", + "--ds-theme-surface-radius", "--ds-theme-surface-opacity", "--ds-theme-surface-blur", + "--ds-theme-surface-border-alpha", "--ds-theme-surface-shadow", "--ds-theme-image-focus-x", + "--ds-theme-image-focus-y", "--ds-theme-image-zoom", "--ds-theme-image-dim", + "--ds-theme-image-task-intensity", "--ds-theme-density-scale", "--ds-theme-motion-level", ]; const installToken = {}; const existingAnalysisCache = window[ANALYSIS_CACHE_KEY]; diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 0c5807f82..0f17caec0 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "20-modern-main-surface"; +const DREAM_SKIN_RENDERER_REVISION: &str = "23-modern-codex-skin-api-compat"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT @@ -202,16 +202,43 @@ fn managed_dream_skin_css(settings: &BackendSettings) -> String { { return String::new(); } - let css_path = Path::new(image_path) + let current_css = Path::new(image_path) .parent() .map(|parent| parent.join("current.css")); - let Some(css_path) = css_path else { - return String::new(); - }; - let Ok(css) = std::fs::read_to_string(css_path) else { + if let Some(css) = current_css.and_then(|path| read_managed_dream_skin_css(&path)) { + return css; + } + + let theme_id = settings.codex_app_dream_skin_theme_config.id.trim(); + if !valid_dream_skin_theme_id(theme_id) { return String::new(); - }; - crate::dream_skin_package::compile_safe_css(&css).unwrap_or_default() + } + let stored_css = crate::paths::default_app_state_dir() + .join("dream-skin/themes") + .join(theme_id) + .join("theme.css"); + read_managed_dream_skin_css(&stored_css).unwrap_or_default() +} + +fn read_managed_dream_skin_css(path: &Path) -> Option { + let metadata = std::fs::symlink_metadata(path).ok()?; + if !metadata.file_type().is_file() + || metadata.file_type().is_symlink() + || metadata.len() > 262_144 + { + return None; + } + let css = std::fs::read_to_string(path).ok()?; + crate::dream_skin_package::compile_safe_css(&css).ok() +} + +fn valid_dream_skin_theme_id(value: &str) -> bool { + let bytes = value.as_bytes(); + (1..=64).contains(&bytes.len()) + && bytes[0].is_ascii_alphanumeric() + && bytes.iter().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'-' | b'_' | b'.') + }) } fn dream_skin_skin_api_bootstrap_script(theme: &str) -> String { @@ -235,21 +262,95 @@ fn dream_skin_skin_api_bootstrap_script(theme: &str) -> String { "--ds-theme-image-focus-y": String(theme?.art?.focusY ?? 0.5), }}; for (const [name, value] of Object.entries(variables)) if (typeof value === "string" && value) root.style.setProperty(name, value); + const apiVariables = {{ + "--ds-theme-font-family": "system", + "--ds-theme-font-scale": "1", + "--ds-theme-surface-radius": "12px", + "--ds-theme-surface-opacity": "1", + "--ds-theme-surface-blur": "0px", + "--ds-theme-surface-border-alpha": "0.14", + "--ds-theme-surface-shadow": "soft", + "--ds-theme-image-focus-x": String(theme?.art?.focusX ?? 0.5), + "--ds-theme-image-focus-y": String(theme?.art?.focusY ?? 0.5), + "--ds-theme-image-zoom": "1", + "--ds-theme-image-dim": "0", + "--ds-theme-image-task-intensity": "0.35", + "--ds-theme-density-scale": "standard", + "--ds-theme-motion-level": "standard", + }}; + for (const [name, value] of Object.entries(apiVariables)) if (typeof value === "string" && value) root.style.setProperty(name, value); + const mainSurfaceMarker = "data-codex-plus-dream-skin-main-surface"; + const ensureMainSurface = () => {{ + const classic = document.querySelector("main.main-surface"); + if (classic) return classic; + const modern = document.querySelector('main[data-app-shell-main-surface], main[class*="MainContentSurface"], main[class*="_MainContentSurface_"]'); + const candidates = modern ? [modern] : [...document.querySelectorAll("main")]; + const shellMain = candidates.length === 1 ? candidates[0] : null; + if (!shellMain) return null; + shellMain.classList.add("main-surface"); + shellMain.setAttribute(mainSurfaceMarker, "true"); + return shellMain; + }}; const map = {{ - root: "html", sidebar: "aside.app-shell-left-panel", main: "main.main-surface", - header: "header.app-header-tint", home: ".dream-skin-home, [data-feature='game-source']", - "home-hero": ".dream-skin-home > div:first-child, [data-feature='game-source']", - "project-list": "[data-feature='game-source']", thread: "main.main-surface [role='main']", - message: "main.main-surface article", composer: ".composer-surface-chrome", - "composer-toolbar": ".composer-surface-chrome [role='toolbar']", dialog: "[role='dialog']", + root: "html", sidebar: "aside.app-shell-left-panel", main: "main:is(.main-surface, [data-app-shell-main-surface], [class*='_MainContentSurface_'])", + header: 'header[data-pip-obstacle="app-shell-header"], header[data-app-shell-header-edge-scroll], header[data-app-shell-header-layout], header[class*="_Header_"], header.app-header-tint', + "home-hero": "[data-feature='game-source'], [data-testid='home-icon']", + "project-list": "[data-feature='game-source']", + message: "main:is(.main-surface, [data-app-shell-main-surface], [class*='_MainContentSurface_']) article, main:is(.main-surface, [data-app-shell-main-surface], [class*='_MainContentSurface_']) [data-message-author-role], main:is(.main-surface, [data-app-shell-main-surface], [class*='_MainContentSurface_']) [data-local-conversation-user-anchor], main:is(.main-surface, [data-app-shell-main-surface], [class*='_MainContentSurface_']) [data-local-conversation-final-assistant]", + composer: ".composer-surface-chrome, [class*='_ComposerLayoutRoot_'], [data-composer-surface-variant][data-composer-radius-variant]", + "composer-toolbar": ".composer-surface-chrome [role='toolbar'], [class*='_ComposerLayoutRoot_'] [class*='_ComposerLayoutFooter_'], [data-composer-surface-variant][data-composer-radius-variant] :is([data-composer-footer-responsive], [class*='_ComposerLayoutFooter_'], [class*='_footer_'])", + dialog: "[role='dialog']", + }}; + const knownParts = new Set([...Object.keys(map), "home", "thread"]); + const clearKnownParts = () => {{ + document.querySelectorAll("[data-ds-part]").forEach((node) => {{ + if (knownParts.has(node.getAttribute("data-ds-part"))) node.removeAttribute("data-ds-part"); + }}); }}; const mark = () => {{ - for (const [part, selector] of Object.entries(map)) for (const node of document.querySelectorAll(selector)) node.setAttribute("data-ds-part", part); + clearKnownParts(); + document.querySelectorAll("[data-ds-thread-surface], [data-ds-thread-scroll]").forEach((node) => {{ + node.removeAttribute("data-ds-thread-surface"); + node.removeAttribute("data-ds-thread-scroll"); + }}); + const shellMain = ensureMainSurface(); + if (!shellMain) return; + for (const [part, selector] of Object.entries(map)) {{ + for (const node of document.querySelectorAll(selector)) node.setAttribute("data-ds-part", part); + }} + + const home = shellMain.querySelector( + '[role="main"].dream-skin-home, [role="main"].dream-home, [role="main"]:has([data-feature="game-source"])' + ); + if (home) home.setAttribute("data-ds-part", "home"); + + const classicThreadNodes = [...shellMain.querySelectorAll('[role="main"]')].filter( + (node) => node !== home && node.querySelector("article, [data-message-id]") + ); + const threadViewport = shellMain.querySelector( + '[data-app-shell-main-content-layout="thread-edge-scroll"]' + ); + const threadScroll = shellMain.querySelector( + '.thread-scroll-container[data-app-action-timeline-scroll], .thread-scroll-container' + ); + const threadSurface = threadViewport || threadScroll || classicThreadNodes[0] || null; + if (threadViewport) threadViewport.setAttribute("data-ds-thread-surface", "true"); + if (threadScroll) threadScroll.setAttribute("data-ds-thread-scroll", "true"); + if (threadSurface) threadSurface.setAttribute("data-ds-part", "thread"); }}; mark(); window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__?.disconnect?.(); const observer = new MutationObserver(() => mark()); - observer.observe(document.documentElement, {{ childList: true, subtree: true }}); + observer.observe(document.documentElement, {{ + childList: true, + subtree: true, + attributes: true, + attributeFilter: [ + "role", "data-feature", "data-app-shell-main-content-layout", + "data-app-action-timeline-scroll", "data-thread-scroll-footer", "data-pip-obstacle", + "data-app-shell-header-layout", "data-app-shell-header-edge-scroll", + ], + }}); window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__ = observer; }})();"#, theme, @@ -470,7 +571,8 @@ pub fn dream_skin_runtime_content_signature(settings: &BackendSettings) -> Strin let (engine, _, css) = dream_skin_target_assets(settings); let theme = serde_json::to_string(&settings.codex_app_dream_skin_theme_config) .expect("dream skin target theme should serialize"); - let style_revision = dream_skin_content_signature(css.as_bytes()); + let managed_css = managed_dream_skin_css(settings); + let style_revision = dream_skin_content_signature(format!("{css}\n{managed_css}").as_bytes()); dream_skin_target_payload_signature(settings, engine, &style_revision, &theme) } diff --git a/crates/codex-plus-core/src/dream_skin_library.rs b/crates/codex-plus-core/src/dream_skin_library.rs index 82a01f520..8f1899228 100644 --- a/crates/codex-plus-core/src/dream_skin_library.rs +++ b/crates/codex-plus-core/src/dream_skin_library.rs @@ -209,6 +209,25 @@ fn stored_theme_image_extension(state_dir: &Path, source: &Path) -> Option Option { + let source = std::fs::canonicalize(source).ok()?; + let themes_dir = std::fs::canonicalize(state_dir.join(THEMES_DIR)).ok()?; + let theme_dir = source.parent()?; + if theme_dir.parent()? != themes_dir { + return None; + } + let theme_id = theme_dir.file_name()?.to_str()?; + if !valid_theme_id(theme_id) { + return None; + } + let css_path = theme_dir.join("theme.css"); + let metadata = std::fs::symlink_metadata(&css_path).ok()?; + (metadata.file_type().is_file() + && !metadata.file_type().is_symlink() + && metadata.len() <= 262_144) + .then_some(css_path) +} + fn valid_theme_id(value: &str) -> bool { let bytes = value.as_bytes(); (1..=64).contains(&bytes.len()) @@ -283,6 +302,18 @@ pub fn save_dream_skin_theme( bail!("Dream Skin theme config exceeds 256 KiB"); } crate::settings::atomic_write(&staging.join(THEME_CONFIG_FILE), &config)?; + if let Some(source_css) = + stored_theme_css_path(state_dir, Path::new(draft.image_path.trim())) + { + let css = std::fs::read_to_string(&source_css).with_context(|| { + format!( + "failed to read Dream Skin Safe CSS {}", + source_css.display() + ) + })?; + crate::dream_skin_package::validate_safe_css(&css)?; + crate::settings::atomic_write(&staging.join("theme.css"), css.as_bytes())?; + } Ok(()) })(); if let Err(error) = staged { diff --git a/crates/codex-plus-core/src/dream_skin_runtime.rs b/crates/codex-plus-core/src/dream_skin_runtime.rs index ebada7887..dcd5ae0ba 100644 --- a/crates/codex-plus-core/src/dream_skin_runtime.rs +++ b/crates/codex-plus-core/src/dream_skin_runtime.rs @@ -413,6 +413,12 @@ pub fn renderer_verification_script() -> &'static str { const home = document.querySelector('[role="main"].dream-home, [role="main"].dream-skin-home, [role="main"].glass-vision-home'); const suggestions = home?.querySelector('.group\\/home-suggestions') || null; const cards = suggestions ? [...suggestions.querySelectorAll('button')].map(box) : []; + const composer = [...document.querySelectorAll( + '.composer-surface-chrome, .composer-footer, [data-thread-scroll-footer], [data-codex-composer], [contenteditable="true"][role="textbox"]' + )] + .map((node) => ({ node, rect: node.getBoundingClientRect(), style: getComputedStyle(node) })) + .filter(({ rect, style }) => rect.width > 200 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden") + .sort((left, right) => right.rect.bottom - left.rect.bottom)[0]?.node || null; const chrome = document.getElementById('codex-dream-skin-chrome') || document.getElementById('codex-glass-vision-skin-chrome'); return JSON.stringify({ @@ -429,7 +435,7 @@ pub fn renderer_verification_script() -> &'static str { hero: box(home?.firstElementChild?.firstElementChild?.firstElementChild), visibleCardCount: cards.filter((item) => item?.visible).length, projectButton: box(home?.querySelector('.group\\/project-selector > button')), - composer: box(document.querySelector('.composer-surface-chrome')), + composer: box(composer), sidebar: box(document.querySelector('aside.app-shell-left-panel')), documentOverflow: { x: document.documentElement.scrollWidth > document.documentElement.clientWidth, diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index e18fda660..8d94a3aa8 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -660,9 +660,8 @@ fn injection_script_installs_dream_skin_from_backend_settings() { assert!(script.contains("--dream-skin-art")); assert!(script.contains("--dream-art")); assert!(script.contains("function refreshDreamSkin()")); - assert!(script.contains( - "codexPlusBackendSettingsLoaded && (!settings.dreamSkinEnabled || settings.dreamSkinPaused)" - )); + assert!(script.contains("const active = settings.dreamSkinEnabled && !settings.dreamSkinPaused")); + assert!(script.contains("codexPlusBackendSettingsLoaded && !active")); assert!(script.contains("window.__CODEX_PLUS_DREAM_SKIN_RUNTIME_REVISION__")); assert!(script.contains("window.__CODEX_PLUS_DREAM_SKIN_ART_SIGNATURE__")); assert!(!script.contains( diff --git a/crates/codex-plus-core/tests/dream_skin_package.rs b/crates/codex-plus-core/tests/dream_skin_package.rs index 33981c8da..d789d703e 100644 --- a/crates/codex-plus-core/tests/dream_skin_package.rs +++ b/crates/codex-plus-core/tests/dream_skin_package.rs @@ -1,7 +1,8 @@ use std::io::{Cursor, Write}; use codex_plus_core::dream_skin_library::{ - load_stored_dream_skin_theme, prepare_dream_skin_activation, save_validated_dream_skin_package, + load_stored_dream_skin_theme, prepare_dream_skin_activation, save_dream_skin_theme, + save_validated_dream_skin_package, }; use codex_plus_core::dream_skin_package::{compile_safe_css, validate_and_read_package}; use serde_json::json; @@ -169,6 +170,27 @@ fn installed_package_preserves_and_activates_safe_css() { ); } +#[test] +fn editing_stored_theme_preserves_safe_css() { + let temp = tempfile::tempdir().unwrap(); + let css = br#"[data-ds-part="thread"] { backdrop-filter: blur(4px); }"#; + let package = validate_and_read_package(&package_bytes("macos", css, None), "macos").unwrap(); + + save_validated_dream_skin_package(temp.path(), &package).unwrap(); + let mut stored = load_stored_dream_skin_theme(temp.path(), "community.theme").unwrap(); + stored.config.name = "Renamed Community Theme".into(); + save_dream_skin_theme(temp.path(), &stored).unwrap(); + + assert_eq!( + std::fs::read( + temp.path() + .join("dream-skin/themes/community.theme/theme.css") + ) + .unwrap(), + css + ); +} + #[test] fn validates_external_dreamskin_fixture_when_provided() { let Ok(path) = std::env::var("DREAM_SKIN_REAL_PACKAGE") else { diff --git a/crates/codex-plus-core/tests/dream_skin_runtime.rs b/crates/codex-plus-core/tests/dream_skin_runtime.rs index 433d36226..b8c401593 100644 --- a/crates/codex-plus-core/tests/dream_skin_runtime.rs +++ b/crates/codex-plus-core/tests/dream_skin_runtime.rs @@ -1,6 +1,7 @@ use codex_plus_core::dream_skin_runtime::{ DreamSkinRuntimeStatus, DreamSkinState, apply_dream_skin_live, macos_arch_name, - parse_renderer_verification, windows_app_path_matches_registered_root, + parse_renderer_verification, renderer_verification_script, + windows_app_path_matches_registered_root, }; use std::path::Path; @@ -119,6 +120,38 @@ fn verification_accepts_target_project_live_contract() { assert!(result.pass); } +#[test] +fn generated_skin_runtime_tracks_modern_codex_surface_contract() { + let settings = codex_plus_core::settings::BackendSettings { + codex_app_dream_skin_enabled: true, + ..Default::default() + }; + let injection = codex_plus_core::assets::injection_script_with_settings(57321, &settings); + let assets_source = + std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/assets.rs")).unwrap(); + + assert!(injection.contains("data-app-shell-main-content-layout=\"thread-edge-scroll\"")); + assert!(injection.contains(".thread-scroll-container[data-app-action-timeline-scroll]")); + assert!(injection.contains("data-pip-obstacle=\"app-shell-header\"")); + assert!(injection.contains("[data-thread-scroll-footer]")); + assert!(injection.contains("data-ds-thread-surface")); + assert!(injection.contains("data-ds-thread-scroll")); + assert!(injection.contains("const threadSurface = threadViewport || threadScroll")); + assert!(injection.contains("data-app-shell-main-surface")); + assert!(injection.contains("data-local-conversation-user-anchor")); + assert!(injection.contains("_ComposerLayoutRoot_")); + assert!(injection.contains("--ds-theme-surface-radius")); + assert!(injection.contains("--ds-theme-surface-opacity")); + assert!(injection.contains("--ds-theme-surface-blur")); + assert!(assets_source.contains("dream-skin/themes")); + assert!(assets_source.contains("join(\"theme.css\")")); + assert!(!assets_source.contains("DREAM_MODERN_CODEX_COMPAT_CSS")); + assert!(renderer_verification_script().contains("[data-codex-composer]")); + assert!( + renderer_verification_script().contains("[contenteditable=\"true\"][role=\"textbox\"]") + ); +} + #[test] fn bundled_skin_runtimes_gate_structural_home_layout_on_classic_chrome() { for relative_path in [ From 22cbe13bd6f2dc87f899674d618e7987cf71a56a Mon Sep 17 00:00:00 2001 From: guicheng Date: Sat, 29 Aug 2026 16:42:05 +0800 Subject: [PATCH 2/7] fix: stop Dream Skin mutation feedback loop --- .../codex-plus-manager/src/dream-skin.test.ts | 4 +- .../cidala-tiger/macos/renderer-inject.js | 2 +- .../dream-skin/macos/renderer-inject.js | 2 +- crates/codex-plus-core/src/assets.rs | 81 +++++++++++-------- 4 files changed, 54 insertions(+), 35 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index f1e17f326..385fc56dd 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -139,7 +139,7 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "23-modern-codex-skin-api-compat"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "24-stable-codex-skin-mutation-sync"/); }); it("bridges the modern Codex Skin API without forcing visual properties", async () => { @@ -160,6 +160,8 @@ describe("dream skin theme helpers", () => { assert.match(assets, /data-app-shell-main-surface/); assert.match(assets, /data-local-conversation-user-anchor/); assert.match(assets, /_ComposerLayoutRoot_/); + assert.match(assets, /desiredParts = new Map/); + assert.match(assets, /desiredThreadSurfaces = new Set/); assert.doesNotMatch(assets, /DREAM_MODERN_CODEX_COMPAT_CSS/); }); diff --git a/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js b/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js index 8a3c70929..3747178fb 100644 --- a/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js +++ b/assets/inject/upstream/cidala-tiger/macos/renderer-inject.js @@ -588,7 +588,7 @@ applyTheme(root, shell); applyArtMetadata(root); applyDecor(root); - root.classList.add("codex-dream-skin"); + if (!root.classList.contains("codex-dream-skin")) root.classList.add("codex-dream-skin"); return shell; }; diff --git a/assets/inject/upstream/dream-skin/macos/renderer-inject.js b/assets/inject/upstream/dream-skin/macos/renderer-inject.js index d3c2c4535..3bd5ee6e5 100644 --- a/assets/inject/upstream/dream-skin/macos/renderer-inject.js +++ b/assets/inject/upstream/dream-skin/macos/renderer-inject.js @@ -559,7 +559,7 @@ setStyleProperty(root, "--dream-skin-art", `url("${artUrl}")`); applyTheme(root, shell); applyArtMetadata(root); - root.classList.add("codex-dream-skin"); + if (!root.classList.contains("codex-dream-skin")) root.classList.add("codex-dream-skin"); return shell; }; diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 0f17caec0..f55e9752a 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "23-modern-codex-skin-api-compat"; +const DREAM_SKIN_RENDERER_REVISION: &str = "24-stable-codex-skin-mutation-sync"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT @@ -302,41 +302,58 @@ fn dream_skin_skin_api_bootstrap_script(theme: &str) -> String { dialog: "[role='dialog']", }}; const knownParts = new Set([...Object.keys(map), "home", "thread"]); - const clearKnownParts = () => {{ - document.querySelectorAll("[data-ds-part]").forEach((node) => {{ - if (knownParts.has(node.getAttribute("data-ds-part"))) node.removeAttribute("data-ds-part"); - }}); - }}; const mark = () => {{ - clearKnownParts(); - document.querySelectorAll("[data-ds-thread-surface], [data-ds-thread-scroll]").forEach((node) => {{ - node.removeAttribute("data-ds-thread-surface"); - node.removeAttribute("data-ds-thread-scroll"); - }}); + const desiredParts = new Map(); + const desiredThreadSurfaces = new Set(); + const desiredThreadScrolls = new Set(); const shellMain = ensureMainSurface(); - if (!shellMain) return; - for (const [part, selector] of Object.entries(map)) {{ - for (const node of document.querySelectorAll(selector)) node.setAttribute("data-ds-part", part); + if (shellMain) {{ + const rememberPart = (node, part) => {{ + if (node) desiredParts.set(node, part); + }}; + for (const [part, selector] of Object.entries(map)) {{ + for (const node of document.querySelectorAll(selector)) rememberPart(node, part); + }} + + const home = shellMain.querySelector( + '[role="main"].dream-skin-home, [role="main"].dream-home, [role="main"]:has([data-feature="game-source"])' + ); + rememberPart(home, "home"); + + const classicThreadNodes = [...shellMain.querySelectorAll('[role="main"]')].filter( + (node) => node !== home && node.querySelector("article, [data-message-id]") + ); + const threadViewport = shellMain.querySelector( + '[data-app-shell-main-content-layout="thread-edge-scroll"]' + ); + const threadScroll = shellMain.querySelector( + '.thread-scroll-container[data-app-action-timeline-scroll], .thread-scroll-container' + ); + const threadSurface = threadViewport || threadScroll || classicThreadNodes[0] || null; + if (threadViewport) desiredThreadSurfaces.add(threadViewport); + if (threadScroll) desiredThreadScrolls.add(threadScroll); + rememberPart(threadSurface, "thread"); }} - const home = shellMain.querySelector( - '[role="main"].dream-skin-home, [role="main"].dream-home, [role="main"]:has([data-feature="game-source"])' - ); - if (home) home.setAttribute("data-ds-part", "home"); - - const classicThreadNodes = [...shellMain.querySelectorAll('[role="main"]')].filter( - (node) => node !== home && node.querySelector("article, [data-message-id]") - ); - const threadViewport = shellMain.querySelector( - '[data-app-shell-main-content-layout="thread-edge-scroll"]' - ); - const threadScroll = shellMain.querySelector( - '.thread-scroll-container[data-app-action-timeline-scroll], .thread-scroll-container' - ); - const threadSurface = threadViewport || threadScroll || classicThreadNodes[0] || null; - if (threadViewport) threadViewport.setAttribute("data-ds-thread-surface", "true"); - if (threadScroll) threadScroll.setAttribute("data-ds-thread-scroll", "true"); - if (threadSurface) threadSurface.setAttribute("data-ds-part", "thread"); + document.querySelectorAll("[data-ds-part]").forEach((node) => {{ + const current = node.getAttribute("data-ds-part"); + if (knownParts.has(current) && desiredParts.get(node) !== current) node.removeAttribute("data-ds-part"); + }}); + for (const [node, part] of desiredParts) {{ + if (node.getAttribute("data-ds-part") !== part) node.setAttribute("data-ds-part", part); + }} + document.querySelectorAll("[data-ds-thread-surface]").forEach((node) => {{ + if (!desiredThreadSurfaces.has(node)) node.removeAttribute("data-ds-thread-surface"); + }}); + for (const node of desiredThreadSurfaces) {{ + if (node.getAttribute("data-ds-thread-surface") !== "true") node.setAttribute("data-ds-thread-surface", "true"); + }} + document.querySelectorAll("[data-ds-thread-scroll]").forEach((node) => {{ + if (!desiredThreadScrolls.has(node)) node.removeAttribute("data-ds-thread-scroll"); + }}); + for (const node of desiredThreadScrolls) {{ + if (node.getAttribute("data-ds-thread-scroll") !== "true") node.setAttribute("data-ds-thread-scroll", "true"); + }} }}; mark(); window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__?.disconnect?.(); From 23939145a64992a9bf46180e1c443a3ff82354be Mon Sep 17 00:00:00 2001 From: guicheng Date: Sat, 29 Aug 2026 17:32:31 +0800 Subject: [PATCH 3/7] perf: reduce renderer scan and layout work --- .../codex-plus-manager/src/dream-skin.test.ts | 2 +- .../src/renderer-inject.test.ts | 13 +- assets/inject/renderer-inject.js | 265 +++++++++++++----- crates/codex-plus-core/src/assets.rs | 2 +- 4 files changed, 212 insertions(+), 70 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index 385fc56dd..40b86e474 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -139,7 +139,7 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "24-stable-codex-skin-mutation-sync"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "25-codex-plus-renderer-performance"/); }); it("bridges the modern Codex Skin API without forcing visual properties", async () => { diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index 7c0275730..92b9758ce 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -263,7 +263,18 @@ describe("renderer injection header compatibility", () => { assert.match(renderer, /_ComposerLayoutRoot_/); assert.match(renderer, /syncDreamSkinComposerDocking/); assert.match(renderer, /data-codex-plus-dreamskin-composer-docked/); - assert.match(renderer, /footer\.style\.setProperty\(property, value, "important"\)/); + assert.match(renderer, /setCodexPlusInlineStyleIfChanged\(footer, property, value, "important"\)/); + }); + + it("coalesces renderer work and keeps layout writes idempotent", async () => { + const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8"); + + assert.match(renderer, /let scanDeferredFrame = 0/); + assert.match(renderer, /if \(!wasRunning \|\| previousContentEl !== conversationViewState\.contentEl/); + assert.match(renderer, /function setCodexPlusInlineStyleIfChanged\(/); + assert.match(renderer, /function setCodexPlusDatasetIfChanged\(/); + assert.match(renderer, /codexServiceTierBadgeRetryAt/); + assert.match(renderer, /scheduleThreadScrollSync\(\);/); }); }); diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 1a67b936b..0ffff4cb9 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -489,6 +489,31 @@ const codexPlusDreamSkinStyleId = "codex-dream-skin-style"; const codexPlusDreamSkinPlatform = String(window.__CODEX_PLUS_DREAM_SKIN_PLATFORM__ || "macos"); const codexPlusDreamSkinRevision = String(window.__CODEX_PLUS_DREAM_SKIN_REVISION__ || "1"); + + function setCodexPlusInlineStyleIfChanged(element, property, value, priority = "") { + if (!element?.style) return; + if (element.style.getPropertyValue(property) === value && element.style.getPropertyPriority(property) === priority) return; + element.style.setProperty(property, value, priority); + } + + function setCodexPlusAttributeIfChanged(element, name, value) { + if (!element) return; + const normalized = String(value); + if (element.getAttribute(name) !== normalized) element.setAttribute(name, normalized); + } + + function setCodexPlusTextIfChanged(element, value) { + if (element && element.textContent !== value) element.textContent = value; + } + + function setCodexPlusDatasetIfChanged(element, name, value) { + if (element && element.dataset[name] !== value) element.dataset[name] = value; + } + + function setCodexPlusBooleanPropertyIfChanged(element, name, value) { + if (element && element[name] !== value) element[name] = value; + } + clearTimeout(window.__codexThreadScrollSaveTimer); window.__codexThreadScrollSaveTimer = null; (window.__codexThreadScrollRestoreTimers || []).forEach((timer) => clearTimeout(timer)); @@ -1865,7 +1890,7 @@ } if (footer.parentElement !== main) main.appendChild(footer); - footer.setAttribute(codexPlusDreamSkinComposerDockAttribute, "true"); + setCodexPlusAttributeIfChanged(footer, codexPlusDreamSkinComposerDockAttribute, "true"); for (const [property, value] of Object.entries({ position: "absolute", top: "auto", @@ -1875,15 +1900,19 @@ width: "auto", "z-index": "10", })) { - footer.style.setProperty(property, value, "important"); + setCodexPlusInlineStyleIfChanged(footer, property, value, "important"); } } function ensureDreamSkinCompanion(theme) { const config = dreamSkinCompanionConfig(theme); + if (!config) { + if (document.getElementById(dreamSkinCompanionId)) removeDreamSkinCompanion(); + return; + } const composer = visibleDreamSkinComposer(); - if (!config || !composer) { - removeDreamSkinCompanion(); + if (!composer) { + if (document.getElementById(dreamSkinCompanionId)) removeDreamSkinCompanion(); return; } @@ -1928,7 +1957,7 @@ : fitsRight || !fitsLeft; if (!fitsRight && !fitsLeft) { - companion.style.opacity = "0"; + setCodexPlusInlineStyleIfChanged(companion, "opacity", "0"); return; } @@ -1939,10 +1968,10 @@ window.innerHeight - renderedHeight - edge, ), ); - companion.style.width = `${config.width}px`; - companion.style.left = `${Math.round(useRight ? right : left)}px`; - companion.style.top = `${Math.round(top)}px`; - companion.style.opacity = "1"; + setCodexPlusInlineStyleIfChanged(companion, "width", `${config.width}px`); + setCodexPlusInlineStyleIfChanged(companion, "left", `${Math.round(useRight ? right : left)}px`); + setCodexPlusInlineStyleIfChanged(companion, "top", `${Math.round(top)}px`); + setCodexPlusInlineStyleIfChanged(companion, "opacity", "1"); } function clearDreamSkinPresentation() { @@ -2318,23 +2347,51 @@ function refreshDreamSkin() { const settings = codexPlusSettings(); const active = settings.dreamSkinEnabled && !settings.dreamSkinPaused; + const root = document.documentElement; if (active) ensureDreamSkinMainSurface(); if (window.__CODEX_PLUS_EXTERNAL_DREAM_SKIN_RUNTIME__) { if (codexPlusBackendSettingsLoaded && !active) { - syncDreamSkinComposerDocking(false); - cleanupDreamSkin(); + const hasPresentation = !!( + window.__CODEX_DREAM_SKIN_STATE__ + || window.__CODEX_GLASS_VISION_SKIN_STATE__ + || root?.classList.contains("codex-dream-skin") + || root?.classList.contains("codex-glass-vision-skin") + || document.getElementById(codexPlusDreamSkinStyleId) + ); + if (hasPresentation) { + syncDreamSkinComposerDocking(false); + cleanupDreamSkin(); + } } else { const theme = window.__CODEX_PLUS_DREAM_SKIN_THEME__ || settings.dreamSkinThemeConfig; const state = window.__CODEX_DREAM_SKIN_STATE__ || window.__CODEX_GLASS_VISION_SKIN_STATE__; - state?.ensure?.(); + const needsEnsure = !!( + state?.ensure + && (!root?.classList.contains("codex-dream-skin") + || !document.getElementById(codexPlusDreamSkinStyleId) + || window.__codexPlusDreamSkinNeedsEnsure) + ); + if (needsEnsure) { + state.ensure(); + window.__codexPlusDreamSkinNeedsEnsure = false; + } if (active) syncDreamSkinComposerDocking(true); ensureDreamSkinCompanion(theme); } return; } if (!active) { - syncDreamSkinComposerDocking(false); - cleanupDreamSkin(); + const hasPresentation = !!( + window.__CODEX_DREAM_SKIN_STATE__ + || window.__CODEX_GLASS_VISION_SKIN_STATE__ + || root?.classList.contains("codex-dream-skin") + || root?.classList.contains("codex-glass-vision-skin") + || document.getElementById(codexPlusDreamSkinStyleId) + ); + if (hasPresentation) { + syncDreamSkinComposerDocking(false); + cleanupDreamSkin(); + } return; } installDreamSkin(settings); @@ -2349,6 +2406,7 @@ } window.__CODEX_PLUS_DREAM_SKIN_ART_SIGNATURE__ = String(payload.artSignature || ""); window.__CODEX_PLUS_DREAM_SKIN_THEME__ = payload.theme && typeof payload.theme === "object" ? payload.theme : {}; + window.__codexPlusDreamSkinNeedsEnsure = true; codexPlusBackendSettings.codexAppDreamSkinEnabled = true; codexPlusBackendSettings.codexAppDreamSkinPaused = false; codexPlusBackendSettings.codexAppDreamSkinThemeConfig = window.__CODEX_PLUS_DREAM_SKIN_THEME__; @@ -2537,6 +2595,8 @@ fastModelName: "", fastSupported: false, }; + let codexServiceTierBadgeRetryAt = 0; + let codexServiceTierBadgeRetryRoute = ""; const codexDefaultServiceTierSetting = { key: "default-service-tier", default: null }; const codexServiceTierFallbackFastValue = "priority"; const codexServiceTierModulePromises = new Map(); @@ -3162,11 +3222,11 @@ function refreshCodexServiceTierBadges() { const state = codexServiceTierBadgeState(); document.querySelectorAll(`[data-codex-service-tier-badge="true"]`).forEach((node) => { - node.dataset.tier = state.tier; - node.dataset.disabled = String(!!state.disabled); - node.textContent = state.label; - node.title = state.title; - node.setAttribute("aria-label", state.title); + setCodexPlusDatasetIfChanged(node, "tier", state.tier); + setCodexPlusDatasetIfChanged(node, "disabled", String(!!state.disabled)); + setCodexPlusTextIfChanged(node, state.label); + if (node.title !== state.title) node.title = state.title; + setCodexPlusAttributeIfChanged(node, "aria-label", state.title); }); } @@ -3183,44 +3243,45 @@ : codexServiceTierFastUnsupportedMessage(fastAvailability.modelName); const fastUnsupportedActive = codexServiceTierState.effectiveMode === "fast" && !fastAvailability.supported; document.querySelectorAll("[data-codex-service-tier-controls]").forEach((node) => { - node.hidden = !featureEnabled; + setCodexPlusBooleanPropertyIfChanged(node, "hidden", !featureEnabled); }); document.querySelectorAll("[data-codex-service-tier-status]").forEach((node) => { - node.dataset.status = fastUnsupportedActive ? "unsupported" : (featureEnabled && backendConnected ? (codexServiceTierState.status || "loading") : (backendChecking ? "loading" : "failed")); - node.textContent = featureEnabled + setCodexPlusDatasetIfChanged(node, "status", fastUnsupportedActive ? "unsupported" : (featureEnabled && backendConnected ? (codexServiceTierState.status || "loading") : (backendChecking ? "loading" : "failed"))); + setCodexPlusTextIfChanged(node, featureEnabled ? (backendConnected ? (codexServiceTierState.message || "未读取") : (backendChecking ? "正在检查后端…" : "未连接")) - : "未启用"; + : "未启用"); }); document.querySelectorAll("[data-codex-service-tier-inherit]").forEach((button) => { - button.disabled = !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"; - button.dataset.active = String(codexServiceTierState.controlMode === "inherit"); + setCodexPlusBooleanPropertyIfChanged(button, "disabled", !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "inherit")); }); document.querySelectorAll("[data-codex-service-tier-standard]").forEach((button) => { - button.disabled = !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"; - button.dataset.active = String(codexServiceTierState.controlMode === "global-standard"); + setCodexPlusBooleanPropertyIfChanged(button, "disabled", !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "global-standard")); }); document.querySelectorAll("[data-codex-service-tier-fast]").forEach((button) => { - button.disabled = fastDisabled; - button.dataset.active = String(codexServiceTierState.controlMode === "global-fast"); - button.title = fastTitle; + setCodexPlusBooleanPropertyIfChanged(button, "disabled", fastDisabled); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "global-fast")); + if (button.title !== fastTitle) button.title = fastTitle; }); document.querySelectorAll("[data-codex-service-tier-custom]").forEach((button) => { - button.disabled = !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"; - button.dataset.active = String(codexServiceTierState.controlMode === "custom"); + setCodexPlusBooleanPropertyIfChanged(button, "disabled", !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "custom")); }); document.querySelectorAll("[data-codex-service-tier-thread-inherit]").forEach((button) => { - button.disabled = !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"; - button.dataset.active = String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "inherit"); - button.title = `当前 thread 不单独覆盖,继承自定义默认 ${codexServiceTierState.defaultMode || "inherit"}`; + setCodexPlusBooleanPropertyIfChanged(button, "disabled", !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "inherit")); + const title = `当前 thread 不单独覆盖,继承自定义默认 ${codexServiceTierState.defaultMode || "inherit"}`; + if (button.title !== title) button.title = title; }); document.querySelectorAll("[data-codex-service-tier-thread-standard]").forEach((button) => { - button.disabled = !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"; - button.dataset.active = String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "standard"); + setCodexPlusBooleanPropertyIfChanged(button, "disabled", !featureEnabled || !backendConnected || codexServiceTierState.status === "loading"); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "standard")); }); document.querySelectorAll("[data-codex-service-tier-thread-fast]").forEach((button) => { - button.disabled = fastDisabled; - button.dataset.active = String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "fast"); - button.title = fastTitle; + setCodexPlusBooleanPropertyIfChanged(button, "disabled", fastDisabled); + setCodexPlusDatasetIfChanged(button, "active", String(codexServiceTierState.controlMode === "custom" && codexServiceTierState.threadMode === "fast")); + if (button.title !== fastTitle) button.title = fastTitle; }); refreshCodexServiceTierBadges(); } @@ -4676,7 +4737,7 @@ } } const status = wrapper.querySelector(".codex-plus-sidebar-nav-status"); - if (status) status.dataset.status = codexPlusBackendStatus.status || "checking"; + if (status) setCodexPlusDatasetIfChanged(status, "status", codexPlusBackendStatus.status || "checking"); const active = !!document.querySelector(`.${codexPlusPageClass}`); setCodexPlusSidebarNavActive(active); } @@ -6226,6 +6287,8 @@ } function updateThreadScrollHandlers() { + if (window.__codexThreadScrollHandlersVersion === codexThreadScrollListenerVersion + && window.__codexThreadScrollHandlers) return; window.__codexThreadScrollHandlers = { shouldBlockAutobottom: shouldBlockThreadScrollAutobottom, shouldBlockIntoView: shouldBlockThreadScrollIntoView, @@ -6237,6 +6300,7 @@ prepareRestoreLock: prepareThreadScrollRestoreLock, scheduleSyncAttempts: scheduleThreadScrollSyncAttempts, }; + window.__codexThreadScrollHandlersVersion = codexThreadScrollListenerVersion; } function installThreadScrollUserIntentCapture() { @@ -6257,6 +6321,9 @@ } function installThreadScrollNavigationCapture() { + if (window.__codexThreadScrollNavigationInstalled === codexThreadScrollRouteHooksVersion + && window.__codexThreadScrollNavigationHandler) return; + window.__codexThreadScrollNavigationInstalled = codexThreadScrollRouteHooksVersion; document.removeEventListener("pointerdown", window.__codexThreadScrollNavigationHandler, true); document.removeEventListener("click", window.__codexThreadScrollClickNavigationHandler, true); document.removeEventListener("keydown", window.__codexThreadScrollKeyboardHandler, true); @@ -6529,6 +6596,7 @@ let codexModelCatalogPromise = null; let codexModelWhitelistRefreshTimer = 0; let codexModelWhitelistRefreshUntil = 0; + let codexModelWhitelistLastRefreshAt = 0; const codexPlusModelListRequestIds = new Set(); if (window.__CODEX_PLUS_TEST_SERVICE_TIER__) { @@ -7177,6 +7245,9 @@ loadCodexModelCatalog(); return; } + const now = Date.now(); + if (now - codexModelWhitelistLastRefreshAt < 1000) return; + codexModelWhitelistLastRefreshAt = now; runCodexModelWhitelistRefreshPass(); } @@ -7461,9 +7532,9 @@ || document.querySelector("header .ms-auto") || nativeShare?.parentElement?.parentElement?.parentElement; if (actionGroup instanceof HTMLElement) { - button.style.position = "static"; - button.style.pointerEvents = "auto"; - button.style.webkitAppRegion = "no-drag"; + setCodexPlusInlineStyleIfChanged(button, "position", "static"); + setCodexPlusInlineStyleIfChanged(button, "pointer-events", "auto"); + setCodexPlusInlineStyleIfChanged(button, "-webkit-app-region", "no-drag"); // 只在按钮还不在操作栏里时才搬动它。过去还要求它必须排在最后, // 一旦 Codex 在它后面挂了别的节点,这个条件就永远成立, // 于是每轮 scan 都 appendChild 一次,反过来又触发下一轮 scan(issue #1960)。 @@ -7477,10 +7548,10 @@ || document.querySelector(selectors.appHeader); if (header instanceof HTMLElement) { // 没有明确操作栏时也保持文档流,避免遮挡原生按钮。 - button.style.position = "static"; - button.style.pointerEvents = "auto"; - button.style.webkitAppRegion = "no-drag"; - button.style.marginLeft = "8px"; + setCodexPlusInlineStyleIfChanged(button, "position", "static"); + setCodexPlusInlineStyleIfChanged(button, "pointer-events", "auto"); + setCodexPlusInlineStyleIfChanged(button, "-webkit-app-region", "no-drag"); + setCodexPlusInlineStyleIfChanged(button, "margin-left", "8px"); if (button.parentElement !== header) header.appendChild(button); } else if (!button.isConnected) { document.body.appendChild(button); @@ -7547,6 +7618,9 @@ } function installSessionShareImportListener() { + if (window.__codexSessionShareImportListenerInstalled === sessionShareButtonVersion + && window.__codexSessionShareImportHandler) return; + window.__codexSessionShareImportListenerInstalled = sessionShareButtonVersion; window.removeEventListener("message", window.__codexSessionShareImportHandler); window.__codexSessionShareImportHandler = (event) => { if (!/^(https:\/\/share\.codexpp\.cc|https:\/\/codexpp-share\.pages\.dev)$/.test(event.origin || "") || event.data?.type !== "codexpp-import-session") return; @@ -7878,7 +7952,7 @@ if (localizeCodexMenuTextNode(node)) changed = true; } if (localizeCodexMenuAttributes(scope)) changed = true; - scope.dataset.codexMenuLocalizationVersion = codexMenuLocalizationVersion; + setCodexPlusDatasetIfChanged(scope, "codexMenuLocalizationVersion", codexMenuLocalizationVersion); } return changed; } @@ -8526,14 +8600,18 @@ function updateDeleteButtonOffsets() { sessionRows().forEach((row) => { + const rowRect = row.getBoundingClientRect(); const hasArchiveConfirm = Array.from(row.querySelectorAll("button")).some((button) => { - const rect = button.getBoundingClientRect(); const label = button.getAttribute("aria-label") || ""; const text = (button.textContent || "").trim(); if (button.classList.contains(buttonClass) || button.classList.contains(exportButtonClass) || label === "归档对话" || label === "置顶对话") return false; - return text === "确认" || (text.length > 0 && rect.width > 0 && rect.width <= 36 && rect.x > row.getBoundingClientRect().right - 50); + if (text === "确认") return true; + const rect = button.getBoundingClientRect(); + return text.length > 0 && rect.width > 0 && rect.width <= 36 && rect.x > rowRect.right - 50; }); - row.classList.toggle("codex-archive-confirm-visible", hasArchiveConfirm); + if (row.classList.contains("codex-archive-confirm-visible") !== hasArchiveConfirm) { + row.classList.toggle("codex-archive-confirm-visible", hasArchiveConfirm); + } }); } @@ -8570,6 +8648,9 @@ } function installDeleteButtonEventDelegation() { + if (window.__codexSessionDeleteDocumentDeleteHandlerVersion === codexDeleteVersion + && window.__codexSessionDeleteDocumentDeleteHandler) return; + window.__codexSessionDeleteDocumentDeleteHandlerVersion = codexDeleteVersion; document.removeEventListener("click", window.__codexSessionDeleteDocumentDeleteHandler, true); const handler = (event) => { const button = event.target?.closest?.(`.${buttonClass}`); @@ -9261,13 +9342,22 @@ removeCodexServiceTierBadges(); return; } + const existingBadges = Array.from(document.querySelectorAll(`[data-codex-service-tier-badge="true"]`)); + if (existingBadges.length > 0) { + refreshCodexServiceTierBadges(); + return; + } + const route = String(location.href || ""); + const now = Date.now(); + if (codexServiceTierBadgeRetryRoute === route && codexServiceTierBadgeRetryAt > now) return; + codexServiceTierBadgeRetryRoute = route; const composer = codexServiceTierFindComposerEl(); const placement = composer ? codexServiceTierBadgePlacement(composer) : null; - const existingBadges = Array.from(document.querySelectorAll(`[data-codex-service-tier-badge="true"]`)); if (!composer || !placement?.parent) { - existingBadges.forEach((badge) => badge.remove()); + codexServiceTierBadgeRetryAt = now + 1000; return; } + codexServiceTierBadgeRetryAt = 0; let badge = existingBadges.find((node) => node.closest?.(".composer-footer") || node.closest?.("button") == null) || existingBadges[0]; existingBadges.forEach((node) => { if (node !== badge) node.remove(); @@ -9375,21 +9465,21 @@ return rect.left + rect.width / 2; } - function conversationViewHasRoomForHtmlCenter(nativeRect, bounds) { + function conversationViewHasRoomForHtmlCenter(nativeRect, bounds, htmlCenter) { if (!nativeRect || !bounds) return false; - const targetLeft = conversationViewHtmlCenter() - nativeRect.width / 2; + const targetLeft = htmlCenter - nativeRect.width / 2; const targetRight = targetLeft + nativeRect.width; return targetLeft >= bounds.left - 0.5 && targetRight <= bounds.right + 0.5; } - function conversationViewAlignElement(el) { + function conversationViewAlignElement(el, htmlCenter) { if (!el?.isConnected) return; conversationViewApplyNativeWidth(el); conversationViewResetOwnOffset(el); const nativeRect = el.getBoundingClientRect(); const bounds = conversationViewSessionRectFor(el); - if (!conversationViewHasRoomForHtmlCenter(nativeRect, bounds)) return; - const targetLeft = conversationViewHtmlCenter() - nativeRect.width / 2; + if (!conversationViewHasRoomForHtmlCenter(nativeRect, bounds, htmlCenter)) return; + const targetLeft = htmlCenter - nativeRect.width / 2; const delta = targetLeft - nativeRect.left; if (Math.abs(delta) > 0.5) { const nextLeft = `${delta.toFixed(2)}px`; @@ -9418,14 +9508,34 @@ ].forEach(conversationViewObserveIfNeeded); } + function conversationViewMutationAffectsLayout(record) { + const content = conversationViewState.contentEl; + const composer = conversationViewState.composerEl; + if (!content?.isConnected || !composer?.isConnected) return true; + const watched = [ + content, + content.parentElement, + content.parentElement?.parentElement, + composer, + composer.parentElement, + composer.parentElement?.parentElement, + ].filter(Boolean); + if (watched.some((node) => node === record.target || node.contains?.(record.target))) return true; + if (record.type !== "childList") return false; + return [...record.addedNodes, ...record.removedNodes].some((node) => + watched.some((parent) => parent === node || parent.contains?.(node)) + ); + } + function conversationViewAlignNow() { if (!codexPlusSettings().conversationView) return; conversationViewResolveTargets(); - conversationViewAlignElement(conversationViewState.contentEl); - conversationViewAlignElement(conversationViewState.composerEl); + const htmlCenter = conversationViewHtmlCenter(); + conversationViewAlignElement(conversationViewState.contentEl, htmlCenter); + conversationViewAlignElement(conversationViewState.composerEl, htmlCenter); } - function scheduleConversationViewAlign(frames = 16) { + function scheduleConversationViewAlign(frames = 3) { conversationViewState.settleFramesLeft = Math.max(conversationViewState.settleFramesLeft, frames); if (conversationViewState.rafId) return; const tick = () => { @@ -9440,6 +9550,13 @@ } function cleanupConversationView() { + if (!conversationViewState.rafId + && !conversationViewState.pollId + && !conversationViewState.mo + && !conversationViewState.ro + && conversationViewState.elements.size === 0 + && !conversationViewState.contentEl + && !conversationViewState.composerEl) return; if (conversationViewState.rafId) cancelAnimationFrame(conversationViewState.rafId); if (conversationViewState.pollId) clearInterval(conversationViewState.pollId); conversationViewState.rafId = 0; @@ -9461,7 +9578,9 @@ function ensureConversationViewRuntime() { if (conversationViewState.ro && conversationViewState.mo && conversationViewState.pollId) return; conversationViewState.ro = conversationViewState.ro || new ResizeObserver(() => scheduleConversationViewAlign()); - conversationViewState.mo = conversationViewState.mo || new MutationObserver(() => scheduleConversationViewAlign()); + conversationViewState.mo = conversationViewState.mo || new MutationObserver((records) => { + if (records.some(conversationViewMutationAffectsLayout)) scheduleConversationViewAlign(); + }); if (document.body && !conversationViewState.moObserved) { conversationViewState.mo.observe(document.body, { childList: true, @@ -9471,7 +9590,7 @@ }); conversationViewState.moObserved = true; } - conversationViewState.pollId = conversationViewState.pollId || window.setInterval(() => scheduleConversationViewAlign(2), 350); + conversationViewState.pollId = conversationViewState.pollId || window.setInterval(() => scheduleConversationViewAlign(1), 1000); } function refreshConversationView() { @@ -9479,8 +9598,14 @@ cleanupConversationView(); return; } + const previousContentEl = conversationViewState.contentEl; + const previousComposerEl = conversationViewState.composerEl; + const wasRunning = !!(conversationViewState.ro && conversationViewState.mo && conversationViewState.pollId); ensureConversationViewRuntime(); - scheduleConversationViewAlign(); + conversationViewResolveTargets(); + if (!wasRunning || previousContentEl !== conversationViewState.contentEl || previousComposerEl !== conversationViewState.composerEl) { + scheduleConversationViewAlign(3); + } } function scanLightweight() { @@ -9504,7 +9629,7 @@ installThreadScrollNavigationCapture(); installThreadScrollUserIntentCapture(); installThreadScrollRouteHooks(); - scheduleThreadScrollSync(true); + scheduleThreadScrollSync(); refreshCodexServiceTierControls(); } @@ -10461,10 +10586,16 @@ } } + let scanDeferredFrame = 0; + function scan() { void installDictationSupportPatch(); runScanStep(scanLightweight); - requestAnimationFrame(() => runScanStep(scanDeferred)); + if (scanDeferredFrame) return; + scanDeferredFrame = requestAnimationFrame(() => { + scanDeferredFrame = 0; + runScanStep(scanDeferred); + }); } function isExtensionUiNode(node) { diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index f55e9752a..ffd3a5d41 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "24-stable-codex-skin-mutation-sync"; +const DREAM_SKIN_RENDERER_REVISION: &str = "25-codex-plus-renderer-performance"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT From 37143181782a6bbb17184129eccfa1cdf84a20af Mon Sep 17 00:00:00 2001 From: guicheng Date: Sat, 29 Aug 2026 17:55:49 +0800 Subject: [PATCH 4/7] fix: stabilize focused EVA composer and toolbar state --- .../codex-plus-manager/src/dream-skin.test.ts | 3 +- .../src/renderer-inject.test.ts | 2 ++ assets/inject/renderer-inject.js | 29 +++++++++++++++---- crates/codex-plus-core/src/assets.rs | 16 ++++++++-- .../codex-plus-core/src/dream_skin_package.rs | 1 + 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index 40b86e474..b1582c05f 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -139,7 +139,7 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "25-codex-plus-renderer-performance"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "26-codex-plus-eva-focus-stability"/); }); it("bridges the modern Codex Skin API without forcing visual properties", async () => { @@ -160,6 +160,7 @@ describe("dream skin theme helpers", () => { assert.match(assets, /data-app-shell-main-surface/); assert.match(assets, /data-local-conversation-user-anchor/); assert.match(assets, /_ComposerLayoutRoot_/); + assert.match(assets, /composer-toolbar-empty/); assert.match(assets, /desiredParts = new Map/); assert.match(assets, /desiredThreadSurfaces = new Set/); assert.doesNotMatch(assets, /DREAM_MODERN_CODEX_COMPAT_CSS/); diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index 92b9758ce..a979c36bb 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -275,6 +275,8 @@ describe("renderer injection header compatibility", () => { assert.match(renderer, /function setCodexPlusDatasetIfChanged\(/); assert.match(renderer, /codexServiceTierBadgeRetryAt/); assert.match(renderer, /scheduleThreadScrollSync\(\);/); + assert.match(renderer, /ownedOffsets: new WeakMap\(\)/); + assert.match(renderer, /Number\.parseFloat\(owned\.left\)/); }); }); diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 0ffff4cb9..a63106561 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -2041,7 +2041,7 @@ delete window.__CODEX_PLUS_DREAM_SKIN_API_OBSERVER__; const registeredParts = new Set([ "root", "sidebar", "main", "header", "home", "home-hero", "project-list", - "thread", "message", "composer", "composer-toolbar", "dialog", + "thread", "message", "composer", "composer-toolbar", "composer-toolbar-empty", "dialog", ]); document.querySelectorAll("[data-ds-part]").forEach((node) => { if (registeredParts.has(node.getAttribute("data-ds-part"))) node.removeAttribute("data-ds-part"); @@ -9153,6 +9153,7 @@ moObserved: false, observed: new WeakSet(), elements: new Set(), + ownedOffsets: new WeakMap(), }; function conversationViewTokenSet(el) { @@ -9475,16 +9476,31 @@ function conversationViewAlignElement(el, htmlCenter) { if (!el?.isConnected) return; conversationViewApplyNativeWidth(el); - conversationViewResetOwnOffset(el); + const owned = conversationViewState.ownedOffsets.get(el); + const ownsCurrentOffset = !!owned + && el.style.left === owned.left + && el.style.transform === owned.transform; + if (!ownsCurrentOffset) conversationViewResetOwnOffset(el); const nativeRect = el.getBoundingClientRect(); const bounds = conversationViewSessionRectFor(el); - if (!conversationViewHasRoomForHtmlCenter(nativeRect, bounds, htmlCenter)) return; + if (!conversationViewHasRoomForHtmlCenter(nativeRect, bounds, htmlCenter)) { + if (ownsCurrentOffset) conversationViewResetOwnOffset(el); + conversationViewState.ownedOffsets.delete(el); + return; + } const targetLeft = htmlCenter - nativeRect.width / 2; const delta = targetLeft - nativeRect.left; - if (Math.abs(delta) > 0.5) { - const nextLeft = `${delta.toFixed(2)}px`; - if (el.style.left !== nextLeft) el.style.left = nextLeft; + if (Math.abs(delta) <= 0.5) { + if (!ownsCurrentOffset) conversationViewState.ownedOffsets.delete(el); + return; } + const currentLeft = ownsCurrentOffset ? Number.parseFloat(owned.left) : 0; + const nextLeft = `${(Number.isFinite(currentLeft) ? currentLeft + delta : delta).toFixed(2)}px`; + if (el.style.left !== nextLeft) el.style.left = nextLeft; + conversationViewState.ownedOffsets.set(el, { + left: nextLeft, + transform: el.style.transform, + }); } function conversationViewObserveIfNeeded(el) { @@ -9569,6 +9585,7 @@ conversationViewState.observed = new WeakSet(); conversationViewState.elements.forEach(conversationViewRestoreElement); conversationViewState.elements.clear(); + conversationViewState.ownedOffsets = new WeakMap(); conversationViewState.contentEl = null; conversationViewState.composerEl = null; } diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index ffd3a5d41..6d0016ef0 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "25-codex-plus-renderer-performance"; +const DREAM_SKIN_RENDERER_REVISION: &str = "26-codex-plus-eva-focus-stability"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT @@ -301,7 +301,7 @@ fn dream_skin_skin_api_bootstrap_script(theme: &str) -> String { "composer-toolbar": ".composer-surface-chrome [role='toolbar'], [class*='_ComposerLayoutRoot_'] [class*='_ComposerLayoutFooter_'], [data-composer-surface-variant][data-composer-radius-variant] :is([data-composer-footer-responsive], [class*='_ComposerLayoutFooter_'], [class*='_footer_'])", dialog: "[role='dialog']", }}; - const knownParts = new Set([...Object.keys(map), "home", "thread"]); + const knownParts = new Set([...Object.keys(map), "home", "thread", "composer-toolbar-empty"]); const mark = () => {{ const desiredParts = new Map(); const desiredThreadSurfaces = new Set(); @@ -333,6 +333,18 @@ fn dream_skin_skin_api_bootstrap_script(theme: &str) -> String { if (threadViewport) desiredThreadSurfaces.add(threadViewport); if (threadScroll) desiredThreadScrolls.add(threadScroll); rememberPart(threadSurface, "thread"); + + const modernComposer = shellMain.querySelector( + '[data-composer-surface-variant][data-composer-radius-variant], [class*="_ComposerLayoutRoot_"]' + ); + const attachments = modernComposer?.querySelector("[data-composer-attachments]"); + const toolbar = modernComposer?.querySelector( + "[data-composer-footer-responsive], [class*='_ComposerLayoutFooter_'], [class*='_footer_']" + ); + if (attachments && toolbar && attachments.children.length === 0 + && desiredParts.get(toolbar) === "composer-toolbar") {{ + desiredParts.set(toolbar, "composer-toolbar-empty"); + }} }} document.querySelectorAll("[data-ds-part]").forEach((node) => {{ diff --git a/crates/codex-plus-core/src/dream_skin_package.rs b/crates/codex-plus-core/src/dream_skin_package.rs index ab50c0fe6..f262fc3f8 100644 --- a/crates/codex-plus-core/src/dream_skin_package.rs +++ b/crates/codex-plus-core/src/dream_skin_package.rs @@ -296,6 +296,7 @@ fn parse_safe_css(css: &str) -> anyhow::Result>> { "message", "composer", "composer-toolbar", + "composer-toolbar-empty", "dialog", ]; let mut parsed = Vec::new(); From 061c7b88d05355242497b550dbf8135a8910af03 Mon Sep 17 00:00:00 2001 From: guicheng Date: Sat, 29 Aug 2026 20:03:14 +0800 Subject: [PATCH 5/7] fix: stabilize modern Codex scroll compositor --- apps/codex-plus-manager/src/dream-skin.test.ts | 2 +- .../upstream/cidala-tiger/macos/dream-skin.css | 13 ++++++++++++- .../inject/upstream/dream-skin/macos/dream-skin.css | 13 ++++++++++++- crates/codex-plus-core/src/assets.rs | 2 +- crates/codex-plus-core/tests/dream_skin_runtime.rs | 10 ++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index b1582c05f..ca939864c 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -139,7 +139,7 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "26-codex-plus-eva-focus-stability"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "27-codex-plus-compositor-stability"/); }); it("bridges the modern Codex Skin API without forcing visual properties", async () => { diff --git a/assets/inject/upstream/cidala-tiger/macos/dream-skin.css b/assets/inject/upstream/cidala-tiger/macos/dream-skin.css index f06f3cc40..4582dd93d 100644 --- a/assets/inject/upstream/cidala-tiger/macos/dream-skin.css +++ b/assets/inject/upstream/cidala-tiger/macos/dream-skin.css @@ -1109,10 +1109,21 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface already owns its readable surface, so retaining this layer creates a false bottom panel and makes the control look duplicated. */ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface - .thread-scroll-container .bg-gradient-to-t.from-token-main-surface-primary { + .thread-scroll-container :is( + .bg-gradient-to-t.from-token-main-surface-primary, + .bg-gradient-to-t.from-surface.via-surface, + [class*="bg-gradient-to-t"][class*="from-surface"][class*="via-surface"] + ) { background: transparent !important; } +/* The modern Codex bottom button sits above a large backdrop-filter surface. + Its native opacity transition can trigger a visible compositor repaint when + scrolling starts, so keep the state change instantaneous under Dream Skin. */ +html.codex-dream-skin [data-thread-scroll-footer] button[class*="end-1/2"][class*="bottom-"] { + transition: none !important; +} + /* Theme the thread Outputs/Sources utility without changing unrelated menus. */ html.codex-dream-skin .dream-skin-outputs-panel { background: diff --git a/assets/inject/upstream/dream-skin/macos/dream-skin.css b/assets/inject/upstream/dream-skin/macos/dream-skin.css index 021811010..1a9d3a983 100644 --- a/assets/inject/upstream/dream-skin/macos/dream-skin.css +++ b/assets/inject/upstream/dream-skin/macos/dream-skin.css @@ -997,10 +997,21 @@ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface already owns its readable surface, so retaining this layer creates a false bottom panel and makes the control look duplicated. */ html.codex-dream-skin[data-dream-art-wide="true"] main.main-surface - .thread-scroll-container .bg-gradient-to-t.from-token-main-surface-primary { + .thread-scroll-container :is( + .bg-gradient-to-t.from-token-main-surface-primary, + .bg-gradient-to-t.from-surface.via-surface, + [class*="bg-gradient-to-t"][class*="from-surface"][class*="via-surface"] + ) { background: transparent !important; } +/* The modern Codex bottom button sits above a large backdrop-filter surface. + Its native opacity transition can trigger a visible compositor repaint when + scrolling starts, so keep the state change instantaneous under Dream Skin. */ +html.codex-dream-skin [data-thread-scroll-footer] button[class*="end-1/2"][class*="bottom-"] { + transition: none !important; +} + .dream-skin-home div:has(> .horizontal-scroll-fade-mask .group\/project-selector) { position: relative; padding-top: 28px !important; diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 6d0016ef0..91fdede4b 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "26-codex-plus-eva-focus-stability"; +const DREAM_SKIN_RENDERER_REVISION: &str = "27-codex-plus-compositor-stability"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT diff --git a/crates/codex-plus-core/tests/dream_skin_runtime.rs b/crates/codex-plus-core/tests/dream_skin_runtime.rs index b8c401593..29ac1e2ad 100644 --- a/crates/codex-plus-core/tests/dream_skin_runtime.rs +++ b/crates/codex-plus-core/tests/dream_skin_runtime.rs @@ -197,6 +197,16 @@ fn bundled_skin_runtimes_gate_structural_home_layout_on_classic_chrome() { || source.contains("data-dream-home-layout=\\\"structured\\\""), "missing soft layout CSS in {relative_path}" ); + if relative_path.ends_with("/macos/dream-skin.css") { + assert!( + source.contains("from-surface.via-surface"), + "missing modern composer fade selector in {relative_path}" + ); + assert!( + source.contains("[data-thread-scroll-footer] button[class*=\"end-1/2\"][class*=\"bottom-\"]"), + "missing modern bottom-button transition guard in {relative_path}" + ); + } } } From cf89da438585720904a5fb21e0b4940830d05641 Mon Sep 17 00:00:00 2001 From: guicheng Date: Sun, 30 Aug 2026 02:07:37 +0800 Subject: [PATCH 6/7] fix: adapt composer to modern side panel layout --- .../codex-plus-manager/src/dream-skin.test.ts | 14 +- .../src/renderer-inject.test.ts | 138 ++++++++++++++++++ assets/inject/renderer-inject.js | 9 +- .../cidala-tiger/macos/dream-skin.css | 46 ++++++ .../upstream/dream-skin/macos/dream-skin.css | 46 ++++++ crates/codex-plus-core/src/assets.rs | 2 +- .../tests/dream_skin_runtime.rs | 9 ++ 7 files changed, 260 insertions(+), 4 deletions(-) diff --git a/apps/codex-plus-manager/src/dream-skin.test.ts b/apps/codex-plus-manager/src/dream-skin.test.ts index ca939864c..3d232ddb9 100644 --- a/apps/codex-plus-manager/src/dream-skin.test.ts +++ b/apps/codex-plus-manager/src/dream-skin.test.ts @@ -139,7 +139,7 @@ describe("dream skin theme helpers", () => { assert.match(compatibility, /shellMain\.classList\.add\("main-surface"\)/); assert.match(compatibility, /data-codex-plus-dream-skin-main-surface/); assert.match(compatibility, /clearDreamSkinMainSurfaceCompatibility\(\)/); - assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "27-codex-plus-compositor-stability"/); + assert.match(assets, /DREAM_SKIN_RENDERER_REVISION: &str = "28-codex-plus-side-panel-layout"/); }); it("bridges the modern Codex Skin API without forcing visual properties", async () => { @@ -187,6 +187,18 @@ describe("dream skin theme helpers", () => { assert.match(css, /\[data-codex-terminal="true"\]/); }); + it("themes the modern Codex right side panel surface", async () => { + const css = await readFile( + new URL("../../../assets/inject/upstream/dream-skin/macos/dream-skin.css", import.meta.url), + "utf8", + ); + + assert.match(css, /\[data-app-shell-tabs="true"\]/); + assert.match(css, /\[data-browser-sidebar-webview-host-root\]/); + assert.match(css, /\[data-browser-sidebar-webview\]/); + assert.match(css, /--app-shell-panel-background/); + }); + it("keeps transient new-chat drafts on native geometry", async () => { const renderer = await readFile( new URL("../../../assets/inject/upstream/dream-skin/windows/renderer-inject.js", import.meta.url), diff --git a/apps/codex-plus-manager/src/renderer-inject.test.ts b/apps/codex-plus-manager/src/renderer-inject.test.ts index a979c36bb..e31e103d9 100644 --- a/apps/codex-plus-manager/src/renderer-inject.test.ts +++ b/apps/codex-plus-manager/src/renderer-inject.test.ts @@ -114,6 +114,132 @@ function installRendererStyle(renderer: string) { return appended; } +class DockStyle { + private readonly values = new Map(); + + setProperty(property: string, value: string, priority = "") { + this.values.set(property, { value, priority }); + } + + getPropertyValue(property: string) { + return this.values.get(property)?.value ?? ""; + } + + getPropertyPriority(property: string) { + return this.values.get(property)?.priority ?? ""; + } + + removeProperty(property: string) { + this.values.delete(property); + } +} + +class DockElement { + readonly children: DockElement[] = []; + readonly style = new DockStyle(); + parentElement: DockElement | null = null; + closestElement: DockElement | null = null; + querySelectorResult: DockElement | null = null; + private readonly attributes = new Map(); + readonly name: string; + + constructor(name: string) { + this.name = name; + } + + get nextSibling(): DockElement | null { + if (!this.parentElement) return null; + const index = this.parentElement.children.indexOf(this); + return index >= 0 ? this.parentElement.children[index + 1] ?? null : null; + } + + appendChild(child: DockElement) { + if (child.parentElement) { + const index = child.parentElement.children.indexOf(child); + if (index >= 0) child.parentElement.children.splice(index, 1); + } + child.parentElement = this; + this.children.push(child); + return child; + } + + querySelector() { + return this.querySelectorResult; + } + + closest() { + return this.closestElement; + } + + getAttribute(name: string) { + return this.attributes.get(name) ?? null; + } + + setAttribute(name: string, value: string) { + this.attributes.set(name, value); + } + + removeAttribute(name: string) { + this.attributes.delete(name); + } +} + +function composerDockRuntime(renderer: string) { + const start = renderer.indexOf(" function visibleDreamSkinComposer()"); + const end = renderer.indexOf("\n function ensureDreamSkinCompanion", start); + assert.ok(start >= 0 && end > start, "composer docking block not found in renderer-inject.js"); + const source = renderer.slice(start, end); + + const main = new DockElement("main"); + const layout = new DockElement("thread-viewport"); + const scroll = new DockElement("thread-scroll"); + const footer = new DockElement("thread-footer"); + main.appendChild(layout); + layout.appendChild(scroll); + scroll.appendChild(footer); + scroll.closestElement = layout; + main.querySelectorResult = scroll; + scroll.querySelectorResult = footer; + + const document = { + querySelector(selector: string) { + return selector.startsWith("main.main-surface") ? main : null; + }, + querySelectorAll() { + return []; + }, + }; + const factory = new Function( + "document", + "setCodexPlusInlineStyleIfChanged", + "setCodexPlusAttributeIfChanged", + `${source}\nreturn { syncDreamSkinComposerDocking };`, + ) as ( + documentValue: typeof document, + setStyle: (element: DockElement, property: string, value: string, priority?: string) => void, + setAttribute: (element: DockElement, name: string, value: string) => void, + ) => { + syncDreamSkinComposerDocking: (enabled: boolean) => void; + }; + return { + ...factory( + document, + (element, property, value, priority = "") => { + if (element.style.getPropertyValue(property) !== value + || element.style.getPropertyPriority(property) !== priority) { + element.style.setProperty(property, value, priority); + } + }, + (element, name, value) => { + if (element.getAttribute(name) !== value) element.setAttribute(name, value); + }, + ), + footer, + main, + layout, + }; +} + describe("renderer injection header compatibility", () => { it("adds the session copy shortcut through the native fork action", async () => { const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8"); @@ -266,6 +392,18 @@ describe("renderer injection header compatibility", () => { assert.match(renderer, /setCodexPlusInlineStyleIfChanged\(footer, property, value, "important"\)/); }); + it("anchors a docked composer footer to the resizable modern thread viewport", async () => { + const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8"); + const runtime = composerDockRuntime(renderer); + + runtime.syncDreamSkinComposerDocking(true); + + assert.equal(runtime.footer.parentElement, runtime.layout); + assert.notEqual(runtime.footer.parentElement, runtime.main); + assert.equal(runtime.footer.style.getPropertyValue("left"), "0"); + assert.equal(runtime.footer.style.getPropertyValue("right"), "0"); + }); + it("coalesces renderer work and keeps layout writes idempotent", async () => { const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8"); diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index a63106561..c924a086f 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -1860,11 +1860,16 @@ ? main?.querySelector(".thread-scroll-container[data-app-action-timeline-scroll], .thread-scroll-container") : null; const renderedFooter = scroll?.querySelector("[data-thread-scroll-footer]") || null; + const dock = enabled + ? scroll?.closest?.( + '[data-app-shell-main-content-layout], [data-ds-thread-surface="true"]', + ) || main + : null; if (state.node && (state.scroll !== scroll || (renderedFooter && renderedFooter !== state.node))) { restoreDreamSkinComposerDock(); } - if (!enabled || !main || !scroll) { + if (!enabled || !main || !scroll || !dock) { if (state.node) restoreDreamSkinComposerDock(); return; } @@ -1889,7 +1894,7 @@ ])); } - if (footer.parentElement !== main) main.appendChild(footer); + if (footer.parentElement !== dock) dock.appendChild(footer); setCodexPlusAttributeIfChanged(footer, codexPlusDreamSkinComposerDockAttribute, "true"); for (const [property, value] of Object.entries({ position: "absolute", diff --git a/assets/inject/upstream/cidala-tiger/macos/dream-skin.css b/assets/inject/upstream/cidala-tiger/macos/dream-skin.css index 4582dd93d..490863f9d 100644 --- a/assets/inject/upstream/cidala-tiger/macos/dream-skin.css +++ b/assets/inject/upstream/cidala-tiger/macos/dream-skin.css @@ -209,6 +209,52 @@ html.codex-dream-skin aside.app-shell-left-panel { html.codex-dream-skin aside.app-shell-left-panel nav { background: transparent !important; } +/* Modern Codex renders the right side panel as a separate app-shell surface + instead of an aside. Keep its native panel variable on the same Dream Skin + surface so the panel chrome does not fall back to opaque white. */ +html.codex-dream-skin + :is([data-app-shell-tabs="true"], [data-browser-sidebar-webview]) { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; + background: var(--app-shell-panel-background) !important; + border-color: var(--ds-immersive-line) !important; + color: var(--ds-text) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + :is([data-app-shell-tabs="true"], [data-browser-sidebar-webview]) { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin [data-browser-sidebar-webview-host-root] { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + [data-browser-sidebar-webview-host-root] { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin + :has(> [data-app-shell-tabs="true"]) { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; + background: var(--app-shell-panel-background) !important; + border-color: var(--ds-immersive-line) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + :has(> [data-app-shell-tabs="true"]) { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin [data-app-shell-tabs="true"] [data-app-shell-tab-row="true"] { + background: transparent !important; + color: var(--ds-text) !important; +} + +html.codex-dream-skin [data-browser-sidebar-webview] > webview { + background: var(--app-shell-panel-background, var(--ds-immersive-sidebar)) !important; +} + html.codex-dream-skin aside.app-shell-left-panel button, html.codex-dream-skin aside.app-shell-left-panel a { color: var(--ds-text) !important; diff --git a/assets/inject/upstream/dream-skin/macos/dream-skin.css b/assets/inject/upstream/dream-skin/macos/dream-skin.css index 1a9d3a983..b2964ccf5 100644 --- a/assets/inject/upstream/dream-skin/macos/dream-skin.css +++ b/assets/inject/upstream/dream-skin/macos/dream-skin.css @@ -204,6 +204,52 @@ html.codex-dream-skin aside.app-shell-left-panel { html.codex-dream-skin aside.app-shell-left-panel nav { background: transparent !important; } +/* Modern Codex renders the right side panel as a separate app-shell surface + instead of an aside. Keep its native panel variable on the same Dream Skin + surface so the panel chrome does not fall back to opaque white. */ +html.codex-dream-skin + :is([data-app-shell-tabs="true"], [data-browser-sidebar-webview]) { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; + background: var(--app-shell-panel-background) !important; + border-color: var(--ds-immersive-line) !important; + color: var(--ds-text) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + :is([data-app-shell-tabs="true"], [data-browser-sidebar-webview]) { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin [data-browser-sidebar-webview-host-root] { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + [data-browser-sidebar-webview-host-root] { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin + :has(> [data-app-shell-tabs="true"]) { + --app-shell-panel-background: var(--ds-immersive-sidebar) !important; + background: var(--app-shell-panel-background) !important; + border-color: var(--ds-immersive-line) !important; +} + +html.codex-dream-skin[data-dream-art-wide="true"] + :has(> [data-app-shell-tabs="true"]) { + --app-shell-panel-background: var(--ds-task-immersive-sidebar) !important; +} + +html.codex-dream-skin [data-app-shell-tabs="true"] [data-app-shell-tab-row="true"] { + background: transparent !important; + color: var(--ds-text) !important; +} + +html.codex-dream-skin [data-browser-sidebar-webview] > webview { + background: var(--app-shell-panel-background, var(--ds-immersive-sidebar)) !important; +} + html.codex-dream-skin aside.app-shell-left-panel button, html.codex-dream-skin aside.app-shell-left-panel a { color: var(--ds-text) !important; diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 91fdede4b..be452eaec 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -52,7 +52,7 @@ const STEPWISE_SCRIPT: &str = concat!( "\n})();\n", ); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; -const DREAM_SKIN_RENDERER_REVISION: &str = "27-codex-plus-compositor-stability"; +const DREAM_SKIN_RENDERER_REVISION: &str = "28-codex-plus-side-panel-layout"; pub fn renderer_script() -> &'static str { RENDERER_SCRIPT diff --git a/crates/codex-plus-core/tests/dream_skin_runtime.rs b/crates/codex-plus-core/tests/dream_skin_runtime.rs index 29ac1e2ad..a16745b0a 100644 --- a/crates/codex-plus-core/tests/dream_skin_runtime.rs +++ b/crates/codex-plus-core/tests/dream_skin_runtime.rs @@ -206,6 +206,15 @@ fn bundled_skin_runtimes_gate_structural_home_layout_on_classic_chrome() { source.contains("[data-thread-scroll-footer] button[class*=\"end-1/2\"][class*=\"bottom-\"]"), "missing modern bottom-button transition guard in {relative_path}" ); + assert!( + source.contains("[data-app-shell-tabs=\"true\"]"), + "missing modern right side panel surface selector in {relative_path}" + ); + assert!( + source.contains("[data-browser-sidebar-webview-host-root]") + && source.contains("[data-browser-sidebar-webview]"), + "missing modern browser side panel background selectors in {relative_path}" + ); } } } From 2eec08ab31012e85668deb4b76e025b68870e71f Mon Sep 17 00:00:00 2001 From: guicheng Date: Mon, 31 Aug 2026 11:57:24 +0800 Subject: [PATCH 7/7] test: update Dream Skin asset baselines --- crates/codex-plus-core/tests/upstream_theme_assets.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/codex-plus-core/tests/upstream_theme_assets.rs b/crates/codex-plus-core/tests/upstream_theme_assets.rs index 9537c227b..387a0a249 100644 --- a/crates/codex-plus-core/tests/upstream_theme_assets.rs +++ b/crates/codex-plus-core/tests/upstream_theme_assets.rs @@ -39,11 +39,11 @@ fn bundled_target_renderers_and_styles_remain_byte_exact() { ), ( "assets/inject/upstream/dream-skin/macos/renderer-inject.js", - "73DA118C964E768676C44C9ABAC910114547DDA44B6190CC3D8A6220059ABB0B", + "ADCD6784F30D694F3418AD17B767DEF5D56D0ED64ECC47296EDA87571BF428AC", ), ( "assets/inject/upstream/dream-skin/macos/dream-skin.css", - "CDA12A5E08815533919A6005A803C2269637CDCAAC4D121D170230163DC9CF09", + "2BFC648BE3C13A07A998E85B4684AC6618C118D178565FE55AD4D4EE9CDE7D98", ), ( "assets/inject/upstream/cidala-tiger/windows/renderer-inject.js", @@ -55,11 +55,11 @@ fn bundled_target_renderers_and_styles_remain_byte_exact() { ), ( "assets/inject/upstream/cidala-tiger/macos/renderer-inject.js", - "19202C8A37C7512E65F950A5516A314867FDF305B74B313F0ABCEA8CF7347F59", + "4EAF24170C6DEF2052F19C7296B64CE12EC1845B595565B0826CD420029BCF2D", ), ( "assets/inject/upstream/cidala-tiger/macos/dream-skin.css", - "45506CA7C71D4E9867287AE2358C4380C0993F0D04039C29FEE6DBEE20495148", + "43D2B65745E7E1291BAE78DADCCE03A35D81AE429EE4911930A7C98DC9B44E99", ), ( "assets/inject/upstream/snow-skin/renderer-inject.js",