diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 63ebc573e..03f6ee9e2 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -468,7 +468,7 @@ const codexThreadServiceTierMaxEntries = 120; const codexThreadServiceTierDraftBindWindowMs = 60 * 1000; const codexServiceTierRequestOverrideVersion = "3"; - const codexAppServerModelRequestPatchVersion = "2"; + const codexAppServerModelRequestPatchVersion = "3"; const codexPluginMarketplaceUnlockVersion = "12"; const codexPluginAutoExpandVersion = "1"; const codexPluginAutoExpandMaxClicks = 80; @@ -2250,25 +2250,40 @@ const codexServiceTierControlModes = new Set(["inherit", "global-standard", "global-fast", "custom"]); ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].forEach((model) => codexServiceTierSupportedFastModels.add(model)); - function codexAppAssetUrl(namePart) { - const urls = [ + function uniqueCodexAppAssetUrls(urls) { + return Array.from(new Set((urls || []).filter((url) => typeof url === "string" && url.includes("/assets/") && url.split("?")[0].endsWith(".js")))); + } + + function codexAppAssetCandidateUrls() { + return uniqueCodexAppAssetUrls([ ...Array.from(document.scripts || []).map((script) => script.src), ...Array.from(document.querySelectorAll("link[href]") || []).map((link) => link.href), ...performance.getEntriesByType("resource").map((entry) => entry.name), - ].filter(Boolean); - return urls.find((url) => url.includes("/assets/") && url.includes(namePart) && url.split("?")[0].endsWith(".js")) || ""; + ]); + } + + function codexAppAssetUrl(namePart) { + if (!namePart) return ""; + return codexAppAssetCandidateUrls().find((url) => url.includes(namePart)) || ""; } async function codexAppAssetUrlFromScriptText(namePart) { - const scripts = Array.from(document.scripts || []).map((script) => script.src).filter(Boolean); + if (!namePart) return ""; + const scripts = codexAppAssetCandidateUrls(); + const escaped = String(namePart).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const patterns = [ + new RegExp(`["'](\\.?/assets/${escaped}[^"']+\\.js)["']`), + new RegExp(`["']([^"']*/assets/${escaped}[^"']+\\.js)["']`), + ]; for (const src of scripts) { - if (!src.includes("/assets/") || !src.split("?")[0].endsWith(".js")) continue; try { const text = await fetch(src).then((response) => response.ok ? response.text() : ""); - const escaped = namePart.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const match = text.match(new RegExp(`["'](\\./assets/${escaped}[^"']+\\.js)["']`)); - if (!match) continue; - return new URL(match[1], src).href; + if (!text) continue; + for (const pattern of patterns) { + const match = text.match(pattern); + if (!match) continue; + return new URL(match[1], src).href; + } } catch { } } @@ -2300,6 +2315,94 @@ } } + function appServerFallbackAssetUrls() { + const urls = codexAppAssetCandidateUrls(); + const preferred = urls.filter((url) => { + const name = (url.split("/").pop() || "").toLowerCase(); + return /use-host-config|app-server-manager-signals|app-initial|app-main|page-|chatg|signals|server-manager|gwqc41kz|c1urrgy0|hsvsqcnf/.test(name); + }); + // Prefer known request-client modules, then the larger application bundles. + preferred.sort((left, right) => { + const score = (url) => { + const name = (url.split("/").pop() || "").toLowerCase(); + if (name.includes("use-host-config")) return 0; + if (name.includes("app-server-manager-signals")) return 1; + if (name.includes("gwqc41kz") || name.includes("c1urrgy0") || name.includes("hsvsqcnf")) return 2; + if (name.includes("app-initial") && name.includes("app-main")) return 3; + if (name.includes("app-main")) return 4; + return 5; + }; + return score(left) - score(right) || right.length - left.length; + }); + return preferred.slice(0, 16); + } + + function collectAppServerRequestCandidatesFromModule(module) { + const candidates = []; + const seen = new Set(); + const push = (value) => { + if (!value || typeof value !== "object" || seen.has(value)) return; + seen.add(value); + candidates.push(value); + }; + for (const value of Object.values(module || {})) { + push(value); + if (!value || typeof value !== "object") continue; + if (typeof value.get === "function") { + try { push(value.get()); } catch {} + try { push(value.get("local")); } catch {} + } + try { + for (const nested of Object.values(value).slice(0, 100)) push(nested); + } catch {} + } + return candidates; + } + + async function loadAppServerRequestModules() { + const modules = []; + const sources = []; + const seenModules = new Set(); + const seenUrls = new Set(); + const pushModule = (module, source) => { + if (!module || typeof module !== "object" || seenModules.has(module)) return; + seenModules.add(module); + modules.push(module); + sources.push(source); + }; + for (const assetPrefix of ["use-host-config-", "app-server-manager-signals-"]) { + try { + const module = await loadOptionalCodexAppModule(assetPrefix); + if (module) pushModule(module, assetPrefix); + } catch { + } + } + for (const url of appServerFallbackAssetUrls()) { + if (seenUrls.has(url)) continue; + seenUrls.add(url); + try { + pushModule(await import(url), url); + } catch { + } + } + return { modules, sources }; + } + + async function loadAppServerRequestCandidates() { + const { modules, sources } = await loadAppServerRequestModules(); + const candidates = []; + const seen = new Set(); + for (const module of modules) { + for (const candidate of collectAppServerRequestCandidatesFromModule(module)) { + if (seen.has(candidate)) continue; + seen.add(candidate); + candidates.push(candidate); + } + } + const usedFallback = sources.some((source) => !source.endsWith("-")); + return { modules, candidates, sources, discovery: usedFallback ? "fallback" : "named-assets" }; + } + async function codexSettingStorageModule() { const module = await loadCodexAppModule("setting-storage-"); if (typeof module.n !== "function" || typeof module.s !== "function") { @@ -4388,28 +4491,26 @@ if (!codexPlusSettings().pluginMarketplaceUnlock) return; const patch = async () => { try { - const module = await loadCodexAppModule("app-server-manager-signals-"); - const candidates = Object.values(module).filter((value) => value && typeof value === "object"); + const { modules, candidates, sources, discovery } = await loadAppServerRequestCandidates(); let patchedCount = 0; for (const candidate of candidates) { if (patchPluginMarketplaceRequestClient(candidate)) patchedCount += 1; - if (typeof candidate.sendRequest !== "function" && typeof candidate.get === "function") { - try { - if (patchPluginMarketplaceRequestClient(candidate.get())) patchedCount += 1; - } catch { - } - } } if (patchedCount > 0) { window.__codexPluginMarketplaceUnlockInstalled = codexPluginMarketplaceUnlockVersion; sendCodexPlusDiagnostic("plugin_marketplace_request_patch_installed", { + moduleCount: modules.length, candidateCount: candidates.length, patchedCount, + sources, + discovery, }); } else { sendCodexPlusDiagnostic("plugin_marketplace_request_patch_not_found", { - exportCount: Object.keys(module || {}).length, + moduleCount: modules.length, candidateCount: candidates.length, + sources, + discovery, }); } } catch (error) { @@ -6447,11 +6548,7 @@ if (appServerModelRequestPatchDisabled) return; const patch = async () => { try { - const modules = []; - for (const assetPrefix of ["use-host-config-", "app-server-manager-signals-"]) { - const module = await loadOptionalCodexAppModule(assetPrefix); - if (module) modules.push({ assetPrefix, module }); - } + const { modules, candidates, sources, discovery } = await loadAppServerRequestCandidates(); if (modules.length === 0) { window.__codexPlusAppServerModelRequestPatchInstalled = codexAppServerModelRequestPatchVersion; sendCodexPlusDiagnostic("model_app_server_request_patch_skipped", { @@ -6459,38 +6556,26 @@ }); return; } - let candidateCount = 0; let patchedCount = 0; - const patchedAssets = []; - for (const { assetPrefix, module } of modules) { - const candidates = Object.values(module).filter((value) => value && typeof value === "object"); - candidateCount += candidates.length; - let assetPatchedCount = 0; - for (const candidate of candidates) { - if (patchAppServerModelRequestClient(candidate)) assetPatchedCount += 1; - if (typeof candidate.sendRequest !== "function" && typeof candidate.get === "function") { - try { - if (patchAppServerModelRequestClient(candidate.get())) assetPatchedCount += 1; - } catch { - } - } - } - if (assetPatchedCount > 0) patchedAssets.push(assetPrefix); - patchedCount += assetPatchedCount; + for (const candidate of candidates) { + if (patchAppServerModelRequestClient(candidate)) patchedCount += 1; } if (patchedCount > 0) { appServerModelRequestPatchMissCount = 0; window.__codexPlusAppServerModelRequestPatchInstalled = codexAppServerModelRequestPatchVersion; sendCodexPlusDiagnostic("model_app_server_request_patch_installed", { - candidateCount, + moduleCount: modules.length, + candidateCount: candidates.length, patchedCount, - assets: patchedAssets, + sources, + discovery, }); } else { noteAppServerModelRequestPatchMiss("model_app_server_request_patch_not_found", { - exportCount: modules.reduce((count, entry) => count + Object.keys(entry.module || {}).length, 0), - candidateCount, - assets: modules.map((entry) => entry.assetPrefix), + moduleCount: modules.length, + candidateCount: candidates.length, + sources, + discovery, }); } } catch (error) { diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index 67b60d0fe..407f29ed6 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -1123,6 +1123,11 @@ fn injection_script_unlocks_custom_model_catalog() { assert!(script.contains("patchStatsigModelDynamicConfig")); assert!(script.contains("patchModelJsonResponse")); assert!(script.contains("installAppServerModelRequestPatch")); + assert!(script.contains("loadAppServerRequestCandidates")); + assert!(script.contains("appServerFallbackAssetUrls")); + assert!(script.contains("collectAppServerRequestCandidatesFromModule")); + assert!(script.contains("codexAppServerModelRequestPatchVersion = \"3\"")); + assert!(script.contains("list-models-for-host")); assert!(script.contains("appServerModelRequestMethod")); assert!(script.contains("send-cli-request-for-host")); @@ -1238,6 +1243,21 @@ fn injection_script_discovers_vscode_api_asset_without_hardcoded_hash() { assert!(!script.contains("import(\"./assets/vscode-api-")); } + +#[test] +fn injection_script_discovers_app_server_request_clients_without_hardcoded_hash() { + let script = assets::injection_script(57321); + + assert!(script.contains("loadAppServerRequestCandidates")); + assert!(script.contains("appServerFallbackAssetUrls")); + assert!(script.contains("[\"use-host-config-\", \"app-server-manager-signals-\"]")); + assert!(script.contains("loadOptionalCodexAppModule(assetPrefix)")); + assert!(script.contains("candidateCount: candidates.length")); + assert!(script.contains("discovery:")); + // Keep legacy lookup as first attempt, but never hardcode the old hashed filename. + assert!(!script.contains("app-server-manager-signals-C1h8B-R-.js") || script.contains("refreshRecentConversationsForHost")); +} + #[test] fn injection_script_clears_project_state_when_moving_to_projectless() { let script = assets::injection_script(57321);