diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx index bc7bfb264..d29fd37e8 100644 --- a/apps/codex-plus-manager/src/App.tsx +++ b/apps/codex-plus-manager/src/App.tsx @@ -3522,7 +3522,7 @@ function EnhanceScreen({ setEnhanceFlag("codexAppThreadScrollRestore", value)} /> - setEnhanceFlag("codexAppStepwiseEnabled", value)} /> + setEnhanceFlag("codexAppStepwiseEnabled", value)} /> setEnhanceFlag("codexAppStepwiseDirectSend", value)} /> diff --git a/apps/codex-plus-manager/src/i18n-en.ts b/apps/codex-plus-manager/src/i18n-en.ts index f58b1f382..ab9514f22 100644 --- a/apps/codex-plus-manager/src/i18n-en.ts +++ b/apps/codex-plus-manager/src/i18n-en.ts @@ -374,8 +374,8 @@ export const EN_PLAIN: Record = { "图片覆盖层": "Image overlay", "在会话列表悬停显示删除按钮,并支持撤销。": "Show a delete button on hover in the session list, with undo support.", "在会话列表显示导出按钮,导出带时间戳的 Markdown。": "Show an export button in the session list to export timestamped Markdown.", - "在 Codex 页面显示可拖动的后续建议浮层;建议由单独配置的 Stepwise API 生成。": - "Shows a draggable follow-up suggestion overlay in the Codex page. Suggestions are generated by a separate Stepwise API.", + "在 Codex 页面显示可拖动的后续建议浮层;建议由单独配置的 Stepwise API 生成。启停后需重启 Codex++ 生效。": + "Shows a draggable follow-up suggestion overlay in the Codex page. Suggestions are generated by a separate Stepwise API. Restart Codex++ after changing this setting.", "在侧边栏会话标题前显示短 ID 和 UUIDv7 创建时间,方便定位历史会话。": "Show a short ID and UUIDv7 creation time before session titles in the sidebar to help locate past sessions.", "基础设置": "Basic settings", diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index f4d4c70d1..7553c0379 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -3540,7 +3540,7 @@ ` : ""}
-
Stepwise
在当前 Codex 页面显示可拖动的下一步建议浮层,可在设置页配置模型和直接发送。
+
Stepwise
在当前 Codex 页面显示可拖动的下一步建议浮层,可在设置页配置模型和直接发送。启停后需重启 Codex++ 生效。
diff --git a/assets/inject/stepwise-inject.js b/assets/inject/stepwise-inject.js index 0b4e0015a..e03ceec54 100644 --- a/assets/inject/stepwise-inject.js +++ b/assets/inject/stepwise-inject.js @@ -62,6 +62,10 @@ return !state.destroyed && window[API_KEY]?.instanceId === INSTANCE_ID; } + function stepwiseEnabled() { + return state.settings?.enabled === true; + } + function normalizeText(value) { return String(value || "") .replace(/\u00a0/g, " ") @@ -914,7 +918,7 @@ } function installFloat() { - if (!isCurrentInstance()) return; + if (!isCurrentInstance() || !stepwiseEnabled()) return; document.querySelectorAll?.(`[${ROOT_ATTR}="true"]`).forEach((node) => { if (node !== state.root) node.remove(); }); @@ -1757,6 +1761,7 @@ } function requestBridgeStepwise(key, userText, assistantText) { + if (!stepwiseEnabled()) return; if (!key || state.bridgePendingHash === key || state.bridgeCache.has(key)) return; state.bridgePendingHash = key; @@ -2029,6 +2034,10 @@ function scan() { if (!isCurrentInstance()) return; + if (!stepwiseEnabled()) { + stopRuntime(); + return; + } state.timer = 0; state.scans += 1; installStyle(); @@ -2106,12 +2115,16 @@ function scheduleScan(delay = SCAN_DELAY_MS) { if (!isCurrentInstance()) return; + if (!stepwiseEnabled()) { + stopRuntime(); + return; + } if (state.timer) window.clearTimeout(state.timer); state.timer = window.setTimeout(scan, delay); } function installObserver() { - if (!isCurrentInstance()) return false; + if (!isCurrentInstance() || !stepwiseEnabled()) return false; const root = document.body || document.documentElement; if (!root) return false; @@ -2147,13 +2160,17 @@ } function activateRuntime() { + if (!stepwiseEnabled()) { + stopRuntime(); + return; + } installStyle(); installFloat(); if (!state.observer && !installObserver()) { document.addEventListener( "DOMContentLoaded", () => { - if (!isCurrentInstance()) return; + if (!isCurrentInstance() || !stepwiseEnabled()) return; installObserver(); installFloat(); void ensureSettings(); diff --git a/crates/codex-plus-core/src/assets.rs b/crates/codex-plus-core/src/assets.rs index 4bc058b8a..bc95a154a 100644 --- a/crates/codex-plus-core/src/assets.rs +++ b/crates/codex-plus-core/src/assets.rs @@ -340,6 +340,11 @@ pub fn injection_script_with_settings(helper_port: u16, settings: &BackendSettin let paste_fix = paste_fix_enabled_config(settings); let force_chinese_locale = force_chinese_locale_config(settings); let fast_startup = fast_startup_config(settings); + let stepwise_runtime = if settings.codex_app_stepwise_enabled { + stepwise_script() + } else { + "" + }; format!( "window.__CODEX_SESSION_DELETE_HELPER__ = {};\nwindow.__CODEX_PLUS_SPONSOR_IMAGES__ = {};\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__ = {};\n{}\n{}\n{}", serde_json::to_string(&helper_url).expect("helper URL should serialize"), @@ -360,7 +365,7 @@ pub fn injection_script_with_settings(helper_port: u16, settings: &BackendSettin .expect("force Chinese locale config should serialize"), serde_json::to_string(&fast_startup).expect("fast startup config should serialize"), renderer_script(), - stepwise_script(), + stepwise_runtime, dream_skin_target_runtime, ) } diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index d988f23f5..a4d60abf8 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -6,6 +6,7 @@ use codex_plus_core::cdp::{ is_quick_chat_page_target, list_targets, pick_injectable_codex_page_target, pick_page_target, validate_cdp_websocket_url, }; +use codex_plus_core::settings::BackendSettings; use futures_util::{SinkExt, StreamExt}; use serde_json::json; @@ -67,6 +68,25 @@ fn injection_script_prefixes_helper_url_and_sponsor_images() { assert!(script.contains("data-codex-plus-discord")); } +#[test] +fn injection_script_omits_stepwise_runtime_when_disabled() { + let script = assets::injection_script_with_settings(57321, &BackendSettings::default()); + + assert!(!script.contains("const API_KEY = \"__codexStepwisePanel\";")); + assert!(script.contains("data-codex-plus-setting=\"stepwise\"")); +} + +#[test] +fn injection_script_includes_stepwise_runtime_when_enabled() { + let settings = BackendSettings { + codex_app_stepwise_enabled: true, + ..Default::default() + }; + let script = assets::injection_script_with_settings(57321, &settings); + + assert!(script.contains("const API_KEY = \"__codexStepwisePanel\";")); +} + #[test] fn pet_real_mouse_settings_are_gated_to_windows_in_injected_ui() { let script = assets::injection_script(57321); @@ -699,7 +719,11 @@ fn injection_script_menu_exposes_marketplace_plugin_switch_only() { #[test] fn injection_script_menu_exposes_stepwise_switch_and_syncs_panel() { - let script = assets::injection_script(57321); + let settings = BackendSettings { + codex_app_stepwise_enabled: true, + ..Default::default() + }; + let script = assets::injection_script_with_settings(57321, &settings); assert!(script.contains("stepwise: false")); assert!(script.contains("stepwise: \"codexAppStepwiseEnabled\"")); @@ -712,6 +736,18 @@ fn injection_script_menu_exposes_stepwise_switch_and_syncs_panel() { assert!(script.contains("activateRuntime();")); } +#[test] +fn stepwise_runtime_stops_work_when_disabled() { + let script = assets::stepwise_script().replace("\r\n", "\n"); + + assert!(script.contains("function stepwiseEnabled()")); + assert!(script.contains("if (!stepwiseEnabled()) {")); + assert!(script.contains("stopRuntime();")); + assert!(script.contains( + "function requestBridgeStepwise(key, userText, assistantText) {\n if (!stepwiseEnabled()) return;" + )); +} + #[test] fn stepwise_direct_send_targets_main_chat_composer() { let script = assets::stepwise_script(); diff --git a/docs/plans/2026-07-17-stepwise-disabled-injection-design.md b/docs/plans/2026-07-17-stepwise-disabled-injection-design.md new file mode 100644 index 000000000..bc3d4008c --- /dev/null +++ b/docs/plans/2026-07-17-stepwise-disabled-injection-design.md @@ -0,0 +1,37 @@ +# Stepwise Disabled Injection Design + +## Goal + +Prevent Codex++ from injecting the Stepwise runtime when Stepwise is disabled, and +ensure a runtime that was already active cannot recreate its floating UI after the +setting is turned off. + +## Current Behavior + +`injection_script_with_settings` always appends `stepwise_script()`. The injected +runtime loads settings later, so a disabled setting blocks the backend request but +does not prevent the Stepwise code from reaching the Codex page. Existing delayed +scan and DOM-ready paths can create UI after the feature is disabled. + +## Design + +The launcher already passes `BackendSettings` to `injection_script_with_settings`. +Use `codex_app_stepwise_enabled` there to append the Stepwise script only when it +is enabled. The renderer menu uses optional chaining for the Stepwise panel, so it +continues to render and persist the toggle when the runtime is absent. + +The Stepwise runtime will additionally guard scan scheduling and DOM-ready work +with `state.settings?.enabled === true`. Its disabled path will call +`stopRuntime()`, which removes the root, style, observer, and timer. + +Changing the setting changes the new-document bundle, so the UI copy will state +that restarting Codex++ is required for the change to take effect. + +## Validation + +- A disabled settings bundle must not contain a Stepwise runtime marker. +- An enabled settings bundle must contain the Stepwise runtime and the renderer + toggle integration. +- The runtime source must show enabled guards before scan, scheduling, and + DOM-ready activation paths. +- Run the targeted core bridge tests and Rust formatting checks.