Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/codex-plus-manager/src/renderer-inject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { readFile } from "node:fs/promises";

describe("renderer injection header compatibility", () => {
it("anchors the Codex++ menu to current and legacy application top bars only", async () => {
const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8");

assert.match(renderer, /appHeader:\s*'[^"]*\[class\*="ApplicationMenuTopBar"\][^']*\.app-header-tint'/);
assert.doesNotMatch(renderer, /document\.querySelector\(["']header["']\)/);
assert.match(renderer, /isApplicationMenuTopBar\s*\?\s*Math\.max\(4, headerRect\.top\)/);
assert.match(renderer, /isApplicationMenuTopBar\s*\?\s*28\s*:\s*headerRect\.height/);
});

it("does not install Codex++ UI in embedded browser documents", async () => {
const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8");

assert.match(renderer, /window\.top\s*!==\s*window/);
assert.match(renderer, /!window\.electronBridge/);
assert.ok(renderer.includes("/^app:\\\/\\\/\\-\\//i.test(window.location.href)"));
assert.match(renderer, /codexPlusIsNodeTestHarness/);
});
});
13 changes: 9 additions & 4 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
(() => {
// The launcher targets the Codex app page, but keep a renderer-side guard
// so this bundle cannot create UI in embedded browser documents.
const codexPlusIsNodeTestHarness = typeof process === "object" && !!process.versions?.node;
if (!codexPlusIsNodeTestHarness && (window.top !== window || window.self !== window || !window.electronBridge || !/^app:\/\/\-\//i.test(window.location.href))) return;
const codexPlusIsWindowsPlatform = /\bWindows\b/i.test(navigator.userAgent || "");

function installCodexPlusFastStartup() {
Expand Down Expand Up @@ -586,7 +590,7 @@
const selectors = {
sidebarThread: "[data-app-action-sidebar-thread-id]",
threadTitle: "[data-thread-title]",
appHeader: ".app-header-tint",
appHeader: '[class*="ApplicationMenuTopBar"], .app-header-tint',
nativeMenuBar: "[class*=\"ms-auto\"][class*=\"flex\"][class*=\"items-center\"]",
headerContextMenuSurface: '[data-testid="app-shell-header-context-menu-surface"]',
archiveNav: 'button[aria-label="已归档对话"], button[aria-label="Archived conversations"]',
Expand Down Expand Up @@ -3888,7 +3892,7 @@

function updateFloatingCodexPlusMenuPosition(menu) {
if (!menu?.classList?.contains(codexPlusMenuFloatingClass)) return;
const header = document.querySelector(selectors.appHeader) || document.querySelector("header");
const header = document.querySelector(selectors.appHeader);
if (!header) return;
const toolbarButtons = Array.from(header.querySelectorAll("button"))
.map((button) => ({ button, rect: button.getBoundingClientRect() }))
Expand All @@ -3907,8 +3911,9 @@

const headerRect = header.getBoundingClientRect();
if (headerRect.height) {
setCssPropIfChanged(menu, "--codex-plus-menu-top", `${headerRect.top}px`);
setCssPropIfChanged(menu, "--codex-plus-menu-height", `${headerRect.height}px`);
const isApplicationMenuTopBar = header.matches?.('[class*="ApplicationMenuTopBar"]');
setCssPropIfChanged(menu, "--codex-plus-menu-top", `${isApplicationMenuTopBar ? Math.max(4, headerRect.top) : headerRect.top}px`);
setCssPropIfChanged(menu, "--codex-plus-menu-height", `${isApplicationMenuTopBar ? 28 : headerRect.height}px`);
}
menu.style.removeProperty("--codex-plus-menu-right");
}
Expand Down
27 changes: 19 additions & 8 deletions crates/codex-plus-core/src/cdp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,18 +253,29 @@ pub fn pick_page_target(targets: &[CdpTarget]) -> anyhow::Result<CdpTarget> {
}

pub fn pick_injectable_codex_page_target(targets: &[CdpTarget]) -> anyhow::Result<CdpTarget> {
for target in targets
.iter()
.filter(|target| is_injectable_page_target(target))
{
if is_primary_codex_page_target(target) {
return Ok(target.clone());
}
// Only inject into Codex's own app:// page (or the supported ChatGPT
// desktop page). Embedded browser pages can have titles or URLs containing
// "Codex" (for example a GitHub PR), but they must never become the target.
if let Some(target) = targets.iter().find(|target| {
is_injectable_page_target(target)
&& is_primary_codex_page_target(target)
&& (is_codex_app_page_target(target)
|| is_chatgpt_desktop_page(&target.title, &target.url))
}) {
return Ok(target.clone());
}

bail!("No injectable Codex page target found")
}

fn is_codex_app_page_target(target: &CdpTarget) -> bool {
let Ok(url) = reqwest::Url::parse(target.url.trim()) else {
return false;
};
url.scheme().eq_ignore_ascii_case("app")
&& url.host_str() == Some("-")
&& url.path().eq_ignore_ascii_case("/index.html")
}

pub fn is_injectable_page_target(target: &CdpTarget) -> bool {
target.target_type == "page"
&& target
Expand Down
2 changes: 2 additions & 0 deletions crates/codex-plus-core/src/user_scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,8 @@ fn wrap_script(script: &UserScriptFile, source: &str) -> String {
format!(
r#"
(() => {{
const codexPlusIsNodeTestHarness = typeof process === "object" && !!process.versions?.node;
if (!codexPlusIsNodeTestHarness && (window.top !== window || window.self !== window || !window.electronBridge || !/^app:\/\/\-\//i.test(window.location.href))) return;
window.__codexPlusUserScripts = window.__codexPlusUserScripts || {{ scripts: {{}} }};
const key = {key};
window.__codexPlusUserScripts.scripts[key] = {{ key, name: {name}, source: {source_name}, status: "loading", error: "", loadedAt: new Date().toISOString() }};
Expand Down
47 changes: 47 additions & 0 deletions crates/codex-plus-core/tests/cdp_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ fn screenshot_command_uses_png_from_surface() {
fn injection_script_prefixes_helper_url_and_sponsor_images() {
let script = assets::injection_script(57321);

assert!(script.contains("!window.electronBridge"));
assert!(script.contains(r#"!/^app:\/\/\-\//i.test(window.location.href)"#));
assert!(script.contains("window.__CODEX_SESSION_DELETE_HELPER__"));
assert!(script.contains("http://127.0.0.1:57321"));
assert!(script.contains("window.__CODEX_PLUS_SPONSOR_IMAGES__"));
Expand Down Expand Up @@ -2285,6 +2287,51 @@ fn pick_injectable_codex_page_target_rejects_non_codex_pages() {
);
}

#[test]
fn pick_injectable_codex_page_target_ignores_embedded_browser_page_named_codex() {
let targets = vec![
target(
"browser-pr",
"page",
"Fix Codex++ menu anchoring · Pull Request",
"https://github.com/BigPizzaV3/CodexPlusPlus/pull/1743",
Some("ws://browser-pr"),
),
target(
"main",
"page",
"Codex",
"app://-/index.html",
Some("ws://main"),
),
];

let picked = pick_injectable_codex_page_target(&targets)
.expect("Codex app page should win over embedded browser content");

assert_eq!(picked.id, "main");
}

#[test]
fn pick_injectable_codex_page_target_rejects_embedded_browser_only_page() {
let targets = vec![target(
"browser-pr",
"page",
"Fix Codex++ menu anchoring · Pull Request",
"https://github.com/BigPizzaV3/CodexPlusPlus/pull/1743",
Some("ws://browser-pr"),
)];

let error = pick_injectable_codex_page_target(&targets)
.expect_err("embedded browser content must not be selected for injection");

assert!(
error
.to_string()
.contains("No injectable Codex page target found")
);
}

#[test]
fn pick_injectable_codex_page_target_accepts_chatgpt_desktop_page() {
let targets = vec![target(
Expand Down
14 changes: 13 additions & 1 deletion crates/codex-plus-core/tests/upstream_theme_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,19 @@ fn assert_sha256(relative_path: &str, expected: &str) {
path.display()
)
});
let actual = format!("{:X}", Sha256::digest(bytes));
// Git may materialize these text assets with CRLF on Windows. Hash the
// repository's canonical LF representation so the guard is platform
// independent while still detecting substantive asset changes.
let mut normalized = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'\r' && bytes.get(index + 1) == Some(&b'\n') {
index += 1;
}
normalized.push(bytes[index]);
index += 1;
}
let actual = format!("{:X}", Sha256::digest(normalized));
assert_eq!(actual, expected, "upstream asset changed: {relative_path}");
}

Expand Down
Loading