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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ apps/codex-plus-manager/src-tauri/gen/
.superpowers/
.claude/
docs/superpowers/
.codex-index/
.context/
.data/
.idea/

*.log
.DS_Store
Expand All @@ -29,4 +33,6 @@ pnpm-workspace.yaml
.trae/
plan.md
plan_v2.md
apps/codex-taskboard/.codex-legacy-bridge/
apps/codex-taskboard/*proof*.png

170 changes: 170 additions & 0 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use codex_plus_core::routes::{BridgeContext, BridgeDataService, BridgeRuntimeSer
use codex_plus_core::user_scripts::UserScriptManager;
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};

#[derive(Clone)]
Expand Down Expand Up @@ -77,6 +78,9 @@ async fn launcher_main() -> Result<()> {
});
let hooks = LauncherHooks::default();
let handle = launch_and_inject_with_hooks(options, &hooks).await?;
if let Ok(settings) = hooks.load_settings().await {
start_taskboard_sidebar_injector_if_enabled(&settings, handle.debug_port);
}
handle.wait_for_codex_exit().await?;
Ok(())
}
Expand Down Expand Up @@ -182,6 +186,14 @@ async fn activate_existing_codex_app(options: &LaunchOptions) -> anyhow::Result<
break;
}
}
if !activated && launch_result.is_ok() {
codex_plus_core::launcher::activate_codex_window_after_launch(
launch_result
.as_ref()
.ok()
.and_then(|launch| launch.process_id()),
);
}
}
let injection_ready = if settings.enhancements_enabled {
hooks
Expand All @@ -191,6 +203,7 @@ async fn activate_existing_codex_app(options: &LaunchOptions) -> anyhow::Result<
false
};
if injection_ready {
start_taskboard_sidebar_injector_if_enabled(&settings, options.debug_port);
hooks
.start_bridge_watchdog(options.debug_port, helper_port)
.await?;
Expand Down Expand Up @@ -244,6 +257,125 @@ fn open_manager_with_update_prompt() -> anyhow::Result<()> {
.map_err(|error| anyhow::anyhow!("启动管理工具失败:{error}"))
}

fn start_taskboard_sidebar_injector_if_enabled(
settings: &codex_plus_core::settings::BackendSettings,
debug_port: u16,
) {
if !settings.enhancements_enabled || !settings.codex_taskboard_enabled {
return;
}
match spawn_taskboard_sidebar_injector(debug_port) {
Ok(Some(process_id)) => {
let _ = codex_plus_core::diagnostic_log::append_diagnostic_log(
"launcher.taskboard_injector_started",
json!({
"debug_port": debug_port,
"process_id": process_id
}),
);
}
Ok(None) => {
let _ = codex_plus_core::diagnostic_log::append_diagnostic_log(
"launcher.taskboard_injector_skipped",
json!({
"debug_port": debug_port,
"message": "Taskboard injector script was not found"
}),
);
}
Err(error) => {
let _ = codex_plus_core::diagnostic_log::append_diagnostic_log(
"launcher.taskboard_injector_failed",
json!({
"debug_port": debug_port,
"message": error.to_string()
}),
);
}
}
}

fn spawn_taskboard_sidebar_injector(debug_port: u16) -> anyhow::Result<Option<u32>> {
let Some(taskboard_root) = taskboard_root_from_current_exe() else {
return Ok(None);
};
let injector = taskboard_root.join("scripts").join("codex-injector.mjs");
let mut command = Command::new(taskboard_node_executable());
command
.current_dir(&taskboard_root)
.arg(injector)
.arg("--daemon")
.arg("--port")
.arg(debug_port.to_string())
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
command.creation_flags(codex_plus_core::windows_create_no_window());
}
let child = command.spawn()?;
Ok(Some(child.id()))
}

fn taskboard_root_from_current_exe() -> Option<PathBuf> {
taskboard_root_from_env().or_else(|| {
std::env::current_exe()
.ok()
.and_then(|path| taskboard_root_for_exe_path(&path))
})
}

fn taskboard_root_from_env() -> Option<PathBuf> {
let root = std::env::var_os("CODEX_TASKBOARD_ROOT").map(PathBuf::from)?;
taskboard_injector_script_exists(&root).then_some(root)
}

fn taskboard_root_for_exe_path(exe_path: &Path) -> Option<PathBuf> {
let start = if exe_path.is_dir() {
exe_path
} else {
exe_path.parent()?
};
for directory in start.ancestors() {
let root = directory.join("apps").join("codex-taskboard");
if taskboard_injector_script_exists(&root) {
return Some(root);
}
}
None
}

fn taskboard_injector_script_exists(root: &Path) -> bool {
root.join("scripts").join("codex-injector.mjs").is_file()
}

fn taskboard_node_executable() -> PathBuf {
if let Some(path) = std::env::var_os("CODEX_TASKBOARD_NODE_EXE").map(PathBuf::from) {
if path.is_file() {
return path;
}
}
if let Some(path) = bundled_codex_node_executable() {
return path;
}
PathBuf::from(if cfg!(windows) { "node.exe" } else { "node" })
}

fn bundled_codex_node_executable() -> Option<PathBuf> {
let home = directories::BaseDirs::new()?.home_dir().to_path_buf();
let node = home
.join(".cache")
.join("codex-runtimes")
.join("codex-primary-runtime")
.join("dependencies")
.join("node")
.join("bin")
.join(if cfg!(windows) { "node.exe" } else { "node" });
node.is_file().then_some(node)
}

fn parse_launch_options<I, S>(args: I) -> LaunchOptions
where
I: IntoIterator<Item = S>,
Expand Down Expand Up @@ -855,6 +987,36 @@ mod tests {
assert_eq!(options.helper_port, LaunchOptions::default().helper_port);
}

#[test]
fn taskboard_root_resolution_finds_dev_tree_from_debug_exe() {
let test_dir = std::env::temp_dir().join(format!(
"codex-plus-taskboard-root-test-{}",
std::process::id()
));
let script = test_dir
.join("apps")
.join("codex-taskboard")
.join("scripts")
.join("codex-injector.mjs");
std::fs::create_dir_all(script.parent().unwrap()).unwrap();
std::fs::write(&script, "").unwrap();
let exe = test_dir
.join("target")
.join("debug")
.join(if cfg!(windows) {
"codex-plus-plus.exe"
} else {
"codex-plus-plus"
});
std::fs::create_dir_all(exe.parent().unwrap()).unwrap();

assert_eq!(
taskboard_root_for_exe_path(&exe),
Some(test_dir.join("apps").join("codex-taskboard"))
);
let _ = std::fs::remove_dir_all(test_dir);
}

#[test]
fn launcher_uses_single_instance_guard_before_launching() {
let source = include_str!("main.rs");
Expand All @@ -864,6 +1026,14 @@ mod tests {
assert!(source.contains("launcher.already_running"));
}

#[test]
fn launcher_retries_existing_codex_window_activation() {
let source = include_str!("main.rs");

assert!(source.contains("activate_codex_window_after_launch"));
assert!(source.contains("if !activated && launch_result.is_ok()"));
}

#[test]
fn launcher_hooks_forward_runtime_watchdogs_and_computer_use_guard_methods() {
let source = include_str!("main.rs");
Expand Down
110 changes: 108 additions & 2 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::collections::BTreeMap;
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::{SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use codex_plus_core::install::SILENT_BINARY;
use codex_plus_core::models::{DeleteResult, SessionRef};
Expand Down Expand Up @@ -53,6 +56,14 @@ pub struct OverviewPayload {
pub logs_path: String,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskboardPayload {
pub url: String,
pub already_running: bool,
pub launched: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct SettingsPayload {
pub settings: BackendSettings,
Expand Down Expand Up @@ -532,6 +543,7 @@ pub fn launch_codex_plus(request: LaunchRequest) -> CommandResult<Value> {
#[tauri::command]
pub fn restart_codex_plus(request: LaunchRequest) -> CommandResult<Value> {
codex_plus_core::watcher::stop_launcher_processes_and_wait();
#[cfg(target_os = "macos")]
codex_plus_core::watcher::stop_codex_processes_for_debug_port_and_wait(request.debug_port);
spawn_codex_plus_launch(request, "Codex 已请求重启,启动任务正在后台运行。")
}
Expand Down Expand Up @@ -579,6 +591,100 @@ fn spawn_silent_launcher(request: &LaunchRequest) -> anyhow::Result<()> {
codex_plus_core::install::spawn_companion(SILENT_BINARY, &args).map(|_| ())
}

const TASKBOARD_PORT: u16 = 47823;
const TASKBOARD_URL: &str = "http://127.0.0.1:47823/?host=codex";

#[tauri::command]
pub fn ensure_taskboard_service() -> CommandResult<TaskboardPayload> {
if taskboard_health_ok() {
return ok(
"Taskboard is already running.",
taskboard_payload(true, false),
);
}

if let Err(error) = spawn_taskboard_service() {
return failed(
&format!("Failed to start Taskboard with codex-taskboard: {error}"),
taskboard_payload(false, false),
);
}

if wait_for_taskboard_health(Duration::from_secs(8)) {
ok("Taskboard started.", taskboard_payload(false, true))
} else {
failed(
"Started codex-taskboard, but http://127.0.0.1:47823/health is still unavailable.",
taskboard_payload(false, true),
)
}
}

fn taskboard_payload(already_running: bool, launched: bool) -> TaskboardPayload {
TaskboardPayload {
url: TASKBOARD_URL.to_string(),
already_running,
launched,
}
}

fn wait_for_taskboard_health(timeout: Duration) -> bool {
let started_at = Instant::now();
while started_at.elapsed() < timeout {
if taskboard_health_ok() {
return true;
}
thread::sleep(Duration::from_millis(250));
}
false
}

fn taskboard_health_ok() -> bool {
let address = SocketAddr::from(([127, 0, 0, 1], TASKBOARD_PORT));
let Ok(mut stream) = TcpStream::connect_timeout(&address, Duration::from_millis(250)) else {
return false;
};
let _ = stream.set_read_timeout(Some(Duration::from_millis(750)));
let _ = stream.set_write_timeout(Some(Duration::from_millis(750)));
if stream
.write_all(b"GET /health HTTP/1.1\r\nHost: 127.0.0.1:47823\r\nConnection: close\r\n\r\n")
.is_err()
{
return false;
}
let mut response = String::new();
stream.read_to_string(&mut response).is_ok()
&& response.contains(" 200 ")
&& response.contains("\"status\":\"ok\"")
}

fn spawn_taskboard_service() -> anyhow::Result<()> {
let mut command = taskboard_start_command();
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
command.creation_flags(0x08000000);
}
command.spawn()?;
Ok(())
}

#[cfg(target_os = "windows")]
fn taskboard_start_command() -> Command {
let mut command = Command::new("cmd");
command.args(["/C", "codex-taskboard"]);
command
}

#[cfg(not(target_os = "windows"))]
fn taskboard_start_command() -> Command {
Command::new("codex-taskboard")
}

#[tauri::command]
pub fn load_settings() -> CommandResult<SettingsPayload> {
settings_payload("设置已加载。", "设置读取失败")
Expand Down
1 change: 1 addition & 0 deletions apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub fn run() {
commands::load_overview,
commands::launch_codex_plus,
commands::restart_codex_plus,
commands::ensure_taskboard_service,
commands::load_settings,
commands::save_settings,
commands::dream_skin_status,
Expand Down
Loading