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
54 changes: 48 additions & 6 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,22 +554,50 @@ impl LaunchHooks for LauncherHooks {
helper_port: u16,
ctx: BridgeContext,
) -> anyhow::Result<()> {
inject_with_context(debug_port, helper_port, ctx, self.runtime.clone()).await
inject_with_context(debug_port, helper_port, ctx, self.runtime.clone(), None).await
}

async fn inject_bridge_for_app(
&self,
debug_port: u16,
helper_port: u16,
ctx: BridgeContext,
app_dir: &Path,
) -> anyhow::Result<()> {
inject_with_context(
debug_port,
helper_port,
ctx,
self.runtime.clone(),
Some(app_dir.to_path_buf()),
)
.await
}

async fn inject(&self, debug_port: u16, helper_port: u16) -> anyhow::Result<()> {
self.core.inject(debug_port, helper_port).await
}

async fn inject_for_app(
&self,
debug_port: u16,
helper_port: u16,
app_dir: &Path,
) -> anyhow::Result<()> {
self.core
.inject_for_app(debug_port, helper_port, app_dir)
.await
}

async fn start_bridge_watchdog(&self, debug_port: u16, helper_port: u16) -> anyhow::Result<()> {
let ctx = self.watchdog_bridge_context()?;
let runtime = self.runtime.clone();
let reinjector: BridgeReinjector = Arc::new(move || {
let ctx = ctx.clone();
let runtime = runtime.clone();
Box::pin(
async move { inject_with_context(debug_port, helper_port, ctx, runtime).await },
)
Box::pin(async move {
inject_with_context(debug_port, helper_port, ctx, runtime, None).await
})
});
self.core.set_bridge_reinjector(reinjector).await;
self.core
Expand Down Expand Up @@ -929,10 +957,19 @@ async fn inject_with_context(
helper_port: u16,
ctx: BridgeContext,
runtime: Arc<LauncherRuntimeService>,
app_dir: Option<PathBuf>,
) -> anyhow::Result<()> {
let mut last_error = None;
for _ in 0..20 {
match try_inject_with_context(debug_port, helper_port, ctx.clone(), runtime.clone()).await {
match try_inject_with_context(
debug_port,
helper_port,
ctx.clone(),
runtime.clone(),
app_dir.as_deref(),
)
.await
{
Ok(()) => return Ok(()),
Err(error) => {
last_error = Some(error);
Expand All @@ -957,6 +994,7 @@ async fn try_inject_with_context(
helper_port: u16,
ctx: BridgeContext,
runtime: Arc<LauncherRuntimeService>,
app_dir: Option<&Path>,
) -> anyhow::Result<()> {
let targets = codex_plus_core::cdp::list_targets(debug_port).await?;
let target = codex_plus_core::cdp::pick_injectable_codex_page_target(&targets)?;
Expand All @@ -968,7 +1006,11 @@ async fn try_inject_with_context(
let settings = codex_plus_core::settings::SettingsStore::default()
.load()
.unwrap_or_default();
let script = codex_plus_core::assets::injection_script_with_settings(helper_port, &settings);
let script = codex_plus_core::assets::injection_script_with_settings_and_app_dir(
helper_port,
&settings,
app_dir,
);
let user_bundle = runtime
.user_scripts
.build_enabled_bundle()
Expand Down
47 changes: 39 additions & 8 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@
const codexServiceTierRequestOverrideVersion = "9";
const codexAppServerModelRequestPatchVersion = "6";
const codexRemoteSessionRecoveryVersion = "5";
const codexPluginMarketplaceUnlockVersion = "15";
const codexPluginMarketplaceUnlockVersion = "16";
const codexThreadScrollMaxEntries = 120;
const codexThreadScrollSaveThrottleMs = 120;
const codexThreadScrollRestoreWindowMs = 3200;
Expand Down Expand Up @@ -4349,7 +4349,11 @@

function pluginMarketplacePluginKey(plugin) {
if (!plugin || typeof plugin !== "object") return "";
return String(plugin.name || plugin.id || plugin.pluginName || "").trim();
const name = String(plugin.name || plugin.pluginName || "").trim();
if (name) return name;
const id = String(plugin.id || "").trim();
const marketplaceSeparator = id.lastIndexOf("@");
return marketplaceSeparator > 0 ? id.slice(0, marketplaceSeparator) : id;
}

function normalizeLocalPluginMarketplacePlugin(plugin, marketplaceName) {
Expand All @@ -4367,19 +4371,36 @@
return cloned;
}

function fillMissingPluginMarketplaceMetadata(target, source) {
if (!target || !source || typeof target !== "object" || typeof source !== "object") return;
for (const key of ["name", "id", "marketplaceName", "marketplacePath", "interface", "keywords", "category", "policy", "source", "installed"]) {
if (target[key] != null || source[key] == null) continue;
const sourceValue = source[key];
target[key] = sourceValue && typeof sourceValue === "object"
? cloneCodexPluginMarketplace(sourceValue)
: sourceValue;
}
}

function mergePluginMarketplacePlugins(target, source) {
if (!target || !source || !Array.isArray(source.plugins)) return 0;
if (!Array.isArray(target.plugins)) target.plugins = [];
const marketplaceName = restorePluginMarketplaceName(target.name || source.name || "");
const existing = new Set(target.plugins.map(pluginMarketplacePluginKey).filter(Boolean));
const existing = new Map(
target.plugins.map((plugin) => [pluginMarketplacePluginKey(plugin), plugin]).filter(([key]) => Boolean(key)),
);
let added = 0;
source.plugins.forEach((plugin) => {
const key = pluginMarketplacePluginKey(plugin);
if (!key || existing.has(key)) return;
const cloned = normalizeLocalPluginMarketplacePlugin(plugin, marketplaceName);
if (!cloned) return;
if (!key || !cloned) return;
const current = existing.get(key);
if (current) {
fillMissingPluginMarketplaceMetadata(current, cloned);
return;
}
target.plugins.push(cloned);
existing.add(key);
existing.set(key, cloned);
added += 1;
});
return added;
Expand All @@ -4400,28 +4421,37 @@
});
let addedMarketplaces = 0;
let addedPlugins = 0;
let addedAppBundledPlugins = 0;
localMarketplaces.forEach((marketplace) => {
const name = restorePluginMarketplaceName(marketplace?.name || "");
if (!name) return;
const appBundled = name === "openai-bundled" && marketplace?.codexPlusSource === "app-bundled";
const existing = byName.get(name);
if (existing) {
addedPlugins += mergePluginMarketplacePlugins(existing, marketplace);
const added = mergePluginMarketplacePlugins(existing, marketplace);
addedPlugins += added;
if (appBundled) addedAppBundledPlugins += added;
return;
}
const cloned = cloneCodexPluginMarketplace(marketplace);
if (!cloned) return;
delete cloned.codexPlusSource;
cloned.plugins = Array.isArray(cloned.plugins)
? cloned.plugins.map((plugin) => normalizeLocalPluginMarketplacePlugin(plugin, name)).filter(Boolean)
: [];
result.marketplaces.push(cloned);
byName.set(name, cloned);
addedMarketplaces += 1;
addedPlugins += Array.isArray(cloned.plugins) ? cloned.plugins.length : 0;
if (appBundled) addedAppBundledPlugins += Array.isArray(cloned.plugins) ? cloned.plugins.length : 0;
});
if (addedMarketplaces > 0 || addedPlugins > 0) {
sendCodexPlusDiagnostic("plugin_marketplace_local_merged", { addedMarketplaces, addedPlugins });
}
return { addedMarketplaces, addedPlugins };
if (addedAppBundledPlugins > 0) {
sendCodexPlusDiagnostic("plugin_marketplace_app_bundled_recovered", { addedPlugins: addedAppBundledPlugins });
}
return { addedMarketplaces, addedPlugins, addedAppBundledPlugins };
}

function restorePluginMarketplaceName(name) {
Expand Down Expand Up @@ -4820,6 +4850,7 @@
patchResponseData: patchPluginMarketplaceResponseData,
remoteAuthError: pluginMarketplaceRemoteAuthError,
localFallback: localPluginMarketplaceFallbackResult,
mergeLocal: mergeLocalPluginMarketplaces,
remoteOnlyFallback: remoteOnlyPluginMarketplaceFallbackResult,
requestProfile: pluginMarketplaceRequestProfile,
isBuildFlavorFilter: isCodexPluginBuildFlavorFilter,
Expand Down
Loading
Loading