diff --git a/assets/inject/floating-panel-visual-inject.js b/assets/inject/floating-panel-visual-inject.js new file mode 100644 index 000000000..4361e2c74 --- /dev/null +++ b/assets/inject/floating-panel-visual-inject.js @@ -0,0 +1,183 @@ +(() => { + "use strict"; + + const API_KEY = "__codexFloatingPanelVisual"; + const PANEL_KEY = "__codexFloatingPanel"; + const MATERIAL_KEY = "codex-floating-panel-material-v3"; + const ROOT_ATTR = "data-codex-floating-panel-root"; + const STYLE_ID = "codex-floating-panel-visual-style"; + const FILTER_ID = "codex-floating-panel-noise-filter"; + const MATERIALS = ["frosted", "clear", "liquid", "crystal", "matte"]; + const instanceId = `visual-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`; + let observer = null; + let themeObserver = null; + let destroyed = false; + let applyFrame = 0; + + function root() { + return document.querySelector(`[${ROOT_ATTR}="true"]`); + } + + function normalizeMaterial(value) { + return MATERIALS.includes(value) ? value : "frosted"; + } + + function material() { + return normalizeMaterial(localStorage.getItem(MATERIAL_KEY)); + } + + function setMaterial(value) { + const next = normalizeMaterial(value); + try { + localStorage.setItem(MATERIAL_KEY, next); + } catch {} + apply(); + return next; + } + + function cycle() { + const current = material(); + const next = MATERIALS[(MATERIALS.indexOf(current) + 1) % MATERIALS.length]; + return setMaterial(next); + } + + function hostTypography() { + const candidate = document.querySelector("[data-codex-intelligence-trigger], textarea, [contenteditable='true'], body"); + if (!candidate) return null; + const style = getComputedStyle(candidate); + return { + family: style.fontFamily || "-apple-system, system-ui, sans-serif", + size: style.fontSize || "13px", + weight: style.fontWeight || "400", + }; + } + + function installFilters() { + if (document.getElementById(FILTER_ID)) return; + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.id = FILTER_ID; + svg.setAttribute("aria-hidden", "true"); + svg.setAttribute("width", "0"); + svg.setAttribute("height", "0"); + svg.style.position = "absolute"; + svg.innerHTML = ` + + + + + + `; + document.body?.appendChild(svg); + } + + function installStyle() { + if (document.getElementById(STYLE_ID)) return; + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + [${ROOT_ATTR}="true"] { + --cfp-font-family: -apple-system, system-ui, "Segoe UI", sans-serif; + --cfp-font-size: 13px; + --cfp-font-weight: 400; + --cfp-material-tint: transparent; + --cfp-material-opacity: .92; + --cfp-material-blur: 24px; + --cfp-material-saturation: 1.08; + --cfp-material-noise: 0; + font-family: var(--cfp-font-family); + font-size: var(--cfp-font-size); + font-weight: var(--cfp-font-weight); + } + [${ROOT_ATTR}="true"] .cfp-shell { + background-color: color-mix(in srgb, var(--cfp-bg) calc(var(--cfp-material-opacity) * 100%), transparent); + background-image: linear-gradient(135deg, var(--cfp-material-tint), transparent 62%); + border-color: color-mix(in srgb, var(--cfp-border) 84%, var(--cfp-material-tint)); + box-shadow: none; + -webkit-backdrop-filter: blur(var(--cfp-material-blur)) saturate(var(--cfp-material-saturation)); + backdrop-filter: blur(var(--cfp-material-blur)) saturate(var(--cfp-material-saturation)); + isolation: isolate; + overflow: hidden; + } + [${ROOT_ATTR}="true"] .cfp-shell::before { + background: rgba(255,255,255,var(--cfp-material-noise)); + content: ""; + inset: 0; + opacity: .55; + pointer-events: none; + position: absolute; + filter: url(#cfp-noise-filter); + } + [${ROOT_ATTR}="true"] .cfp-shell > * { position: relative; z-index: 1; } + [${ROOT_ATTR}="true"][data-material="frosted"] { --cfp-material-tint: rgba(255,255,255,.18); --cfp-material-opacity: .88; --cfp-material-blur: 20px; --cfp-material-noise: .06; } + [${ROOT_ATTR}="true"][data-material="clear"] { --cfp-material-tint: rgba(255,255,255,.08); --cfp-material-opacity: .34; --cfp-material-blur: 10px; --cfp-material-saturation: 1.2; --cfp-material-noise: .04; } + [${ROOT_ATTR}="true"][data-material="liquid"] { --cfp-material-tint: rgba(111,169,255,.2); --cfp-material-opacity: .54; --cfp-material-blur: 16px; --cfp-material-saturation: 1.25; --cfp-material-noise: .09; } + [${ROOT_ATTR}="true"][data-material="crystal"] { --cfp-material-tint: rgba(206,229,255,.24); --cfp-material-opacity: .62; --cfp-material-blur: 12px; --cfp-material-saturation: 1.3; --cfp-material-noise: .08; } + [${ROOT_ATTR}="true"][data-material="matte"] { --cfp-material-tint: rgba(80,87,101,.12); --cfp-material-opacity: .82; --cfp-material-blur: 4px; --cfp-material-saturation: .92; --cfp-material-noise: .025; } + [${ROOT_ATTR}="true"] .cfp-status-face[data-pending="true"], + [${ROOT_ATTR}="true"] .cfp-status-face[data-pending="true"] ~ .cfp-capsule-copy .cfp-capsule-label { + background: linear-gradient(90deg, currentColor 35%, var(--cfp-accent) 50%, currentColor 65%); + background-clip: text; + background-size: 220% 100%; + color: transparent; + -webkit-background-clip: text; + animation: cfp-shimmer 1.5s linear infinite; + } + @keyframes cfp-shimmer { from { background-position: 180% 0; } to { background-position: -20% 0; } } + @media (prefers-reduced-motion: reduce) { + [${ROOT_ATTR}="true"] .cfp-status-face[data-pending="true"], + [${ROOT_ATTR}="true"] .cfp-status-face[data-pending="true"] ~ .cfp-capsule-copy .cfp-capsule-label { animation: none; color: var(--cfp-muted); } + } + `; + document.documentElement.appendChild(style); + } + + function apply() { + if (destroyed) return; + const panelRoot = root(); + if (!panelRoot) return; + panelRoot.dataset.material = material(); + const typography = hostTypography(); + if (typography) { + panelRoot.style.setProperty("--cfp-font-family", typography.family); + panelRoot.style.setProperty("--cfp-font-size", typography.size); + panelRoot.style.setProperty("--cfp-font-weight", typography.weight); + } + const pending = String(Boolean(panelRoot.querySelector(".cfp-capsule-label")?.textContent?.includes("正在"))); + const statusFace = panelRoot.querySelector(".cfp-status-face"); + if (statusFace?.dataset.pending !== pending) statusFace?.setAttribute("data-pending", pending); + if (panelRoot.dataset.visualLayer !== "true") panelRoot.dataset.visualLayer = "true"; + } + + function scheduleApply() { + if (applyFrame || destroyed) return; + applyFrame = window.requestAnimationFrame(() => { + applyFrame = 0; + apply(); + }); + } + + function destroy() { + destroyed = true; + if (applyFrame) window.cancelAnimationFrame(applyFrame); + observer?.disconnect(); + themeObserver?.disconnect(); + document.getElementById(STYLE_ID)?.remove(); + document.getElementById(FILTER_ID)?.remove(); + if (window[API_KEY]?.instanceId === instanceId) delete window[API_KEY]; + } + + function start() { + if (destroyed) return; + installFilters(); + installStyle(); + apply(); + observer = new MutationObserver(scheduleApply); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + themeObserver = new MutationObserver(scheduleApply); + themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style"] }); + } + + window[API_KEY] = { version: "2.0.0", instanceId, destroy, apply, current: () => ({ material: material() }), cycle, setMaterial }; + if (document.body) start(); + else document.addEventListener("DOMContentLoaded", start, { once: true }); +})(); diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 04068bedf..bbd067caf 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -46,6 +46,8 @@ const DREAM_SKIN_DEFAULT_IMAGE: &[u8] = include_bytes!("../../../assets/inject/upstream/dream-skin/macos/portal-hero.png"); const PET_REAL_MOUSE_SCRIPT: &str = include_str!("../../../assets/inject/pet-real-mouse-inject.js"); const STEPWISE_SCRIPT: &str = include_str!("../../../assets/inject/stepwise-inject.js"); +const FLOATING_PANEL_VISUAL_SCRIPT: &str = + include_str!("../../../assets/inject/floating-panel-visual-inject.js"); pub const DIAGNOSTIC_BUILD_ID: &str = "diag-20260518-1"; const DREAM_SKIN_RENDERER_REVISION: &str = "20-modern-main-surface"; @@ -270,6 +272,10 @@ pub fn stepwise_script() -> &'static str { STEPWISE_SCRIPT } +pub fn floating_panel_visual_script() -> &'static str { + FLOATING_PANEL_VISUAL_SCRIPT +} + pub fn pet_real_mouse_script() -> &'static str { PET_REAL_MOUSE_SCRIPT } @@ -405,9 +411,9 @@ pub fn injection_script_with_settings(helper_port: u16, settings: &BackendSettin let fast_startup = fast_startup_config(settings); let hide_official_usage_alert = hide_official_usage_alert_config(settings); let stepwise_runtime = if settings.codex_app_stepwise_enabled { - stepwise_script() + format!("{}\n{}", stepwise_script(), floating_panel_visual_script()) } else { - "" + String::new() }; format!( "window.__CODEX_SESSION_DELETE_HELPER__ = {};\nwindow.__CODEX_PLUS_VERSION__ = {};\nwindow.__CODEX_PLUS_BUILD__ = {};\nwindow.__CODEX_PLUS_IMAGE_OVERLAY__ = {};\nwindow.__CODEX_PLUS_PLUGIN_MARKETPLACES__ = {};\nwindow.__CODEX_PLUS_EXTERNAL_DREAM_SKIN_RUNTIME__ = true;\nwindow.__CODEX_PLUS_DREAM_SKIN_PLATFORM__ = {};\nwindow.__CODEX_PLUS_DREAM_SKIN_REVISION__ = {};\nwindow.__CODEX_PLUS_DREAM_SKIN_ART__ = {};\nwindow.__CODEX_PLUS_DREAM_SKIN_ART_SIGNATURE__ = {};\nwindow.__CODEX_PLUS_DREAM_SKIN_THEME__ = {};\nwindow.__CODEX_PLUS_PASTE_FIX__ = {};\nwindow.__CODEX_PLUS_FORCE_CHINESE_LOCALE__ = {};\nwindow.__CODEX_PLUS_FAST_STARTUP__ = {};\nwindow.__CODEX_PLUS_HIDE_OFFICIAL_USAGE_ALERT__ = {};\n{}\n{}\n{}", diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index 49a4b4da2..1ef3fd2aa 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -89,6 +89,39 @@ fn injection_script_includes_stepwise_runtime_when_enabled() { assert!(script.contains("const API_KEY = \"__codexStepwisePanel\";")); } +#[test] +fn floating_panel_visual_layer_is_opt_in_and_host_adaptive() { + let disabled = assets::injection_script_with_settings(57321, &BackendSettings::default()); + assert!(!disabled.contains("__codexFloatingPanelVisual")); + + let enabled = assets::injection_script_with_settings( + 57321, + &BackendSettings { + codex_app_stepwise_enabled: true, + ..Default::default() + }, + ); + assert!(enabled.contains("__codexFloatingPanelVisual")); + assert!(enabled.contains("cfp-noise-filter")); + assert!(enabled.contains("prefers-reduced-motion")); + assert!(enabled.contains("box-shadow: none")); +} + +#[test] +fn injection_script_includes_visual_layer_only_with_stepwise() { + let disabled = assets::injection_script_with_settings(57321, &BackendSettings::default()); + assert!(!disabled.contains("__codexFloatingPanelVisual")); + + let enabled = BackendSettings { + codex_app_stepwise_enabled: true, + ..Default::default() + }; + let script = assets::injection_script_with_settings(57321, &enabled); + assert!(script.contains("__codexFloatingPanelVisual")); + assert!(script.contains("cfp-noise-filter")); + assert!(script.contains("prefers-reduced-motion")); +} + #[test] fn pet_real_mouse_settings_are_gated_to_windows_in_injected_ui() { let script = assets::injection_script(57321);