From 8679147a4dfba83a4440521cd794e7d33b89fe9a Mon Sep 17 00:00:00 2001 From: lwc <840418588@qq.com> Date: Tue, 11 Aug 2026 20:11:50 +0800 Subject: [PATCH 1/4] feat: integrate taskboard into CodexPlusPlus --- .gitignore | 6 + apps/codex-plus-launcher/src/main.rs | 170 + .../src-tauri/src/commands.rs | 110 +- apps/codex-plus-manager/src-tauri/src/lib.rs | 1 + .../src-tauri/tests/windows_subsystem.rs | 4 +- .../src-tauri/windows-app-manifest.xml | 2 +- apps/codex-plus-manager/src/App.tsx | 99 +- apps/codex-plus-manager/src/styles.css | 75 + apps/codex-taskboard/.gitignore | 13 + apps/codex-taskboard/README.md | 141 + apps/codex-taskboard/cli/taskctl.mjs | 855 ++ .../cloud/migrations/0001_initial.sql | 33 + .../cloud/migrations/0002_project_archive.sql | 2 + apps/codex-taskboard/cloud/src/index.mjs | 2274 ++++ .../docs/cloud-collaboration.md | 194 + .../inject/codex-taskboard.user.js | 1840 ++++ apps/codex-taskboard/package-lock.json | 4647 ++++++++ apps/codex-taskboard/package.json | 52 + .../scripts/codex-injector-runtime.mjs | 165 + .../scripts/codex-injector.mjs | 1417 +++ .../scripts/codex-rate-limits.mjs | 156 + apps/codex-taskboard/scripts/dev.mjs | 28 + .../scripts/migrate-to-cloud.mjs | 787 ++ .../scripts/wrangler-cloud-adapter.mjs | 213 + .../server/ai-chat-catalog.mjs | 256 + .../server/ai-chat-process.mjs | 465 + apps/codex-taskboard/server/ai-chat.mjs | 585 + apps/codex-taskboard/server/app.mjs | 2341 ++++ apps/codex-taskboard/server/cloud-config.mjs | 188 + apps/codex-taskboard/server/cloud-proxy.mjs | 261 + apps/codex-taskboard/server/database.mjs | 1707 +++ apps/codex-taskboard/server/index.mjs | 38 + apps/codex-taskboard/shared/domain.mjs | 21 + .../shared/taskboard-automation-options.d.mts | 44 + .../shared/taskboard-automation-options.mjs | 68 + .../shared/taskboard-automation.mjs | 181 + .../shared/workflow-control-flow.d.mts | 159 + .../shared/workflow-control-flow.mjs | 723 ++ .../shared/workflow-sequence.d.mts | 65 + .../shared/workflow-sequence.mjs | 226 + .../skills/manage-taskboard/SKILL.md | 33 + .../manage-taskboard/agents/openai.yaml | 4 + .../skills/manage-taskboard/references/cli.md | 151 + .../test/actor-identity.test.mjs | 84 + .../codex-taskboard/test/ai-chat-api.test.mjs | 176 + .../test/ai-chat-database.test.mjs | 206 + .../test/ai-chat-runner.test.mjs | 403 + .../test/ai-chat-server.test.mjs | 326 + .../test/ai-chat-state.test.mjs | 282 + apps/codex-taskboard/test/ai-chat-ui.test.mjs | 231 + .../test/board-interactions.test.mjs | 201 + .../codex-taskboard/test/board-views.test.mjs | 259 + apps/codex-taskboard/test/cli.test.mjs | 517 + .../test/cloud-companion.test.mjs | 762 ++ .../test/cloud-migration.test.mjs | 1199 +++ .../test/cloud-revision-polling.test.mjs | 239 + .../test/cloud-shared-worker.test.mjs | 620 ++ .../test/comment-activity-style.test.mjs | 38 + .../test/helpers/cloud-worker-harness.mjs | 115 + .../test/hidden-columns.test.mjs | 79 + .../codex-taskboard/test/icon-system.test.mjs | 39 + apps/codex-taskboard/test/inject.test.mjs | 346 + .../test/injector-host-runtime.test.mjs | 187 + apps/codex-taskboard/test/injector.test.mjs | 102 + .../test/input-focus-chrome.test.mjs | 60 + .../test/issue-assignee.test.mjs | 63 + .../test/issue-detail-inline-focus.test.mjs | 23 + .../test/issue-detail-markdown.test.mjs | 80 + .../test/issue-detail-route.test.mjs | 56 + .../test/issue-relations.test.mjs | 77 + .../test/manage-taskboard-skill.test.mjs | 20 + .../test/project-automation-settings.test.mjs | 158 + .../test/project-home.test.mjs | 124 + apps/codex-taskboard/test/server.test.mjs | 1934 ++++ .../test/taskboard-automation.test.mjs | 437 + .../test/workflow-condition-branches.test.mjs | 115 + .../test/workflow-condition-layout.test.mjs | 64 + .../workflow-control-flow-layout.test.mjs | 223 + .../test/workflow-control-flow.test.mjs | 653 ++ .../test/workflow-sequence.test.mjs | 89 + .../test/workflow-twitter-node.test.mjs | 56 + .../test/zapier-workflow.test.mjs | 490 + apps/codex-taskboard/web/index.html | 18 + .../web/public/Inter-LICENSE.txt | 92 + .../web/public/InterVariable.woff2 | Bin 0 -> 352240 bytes .../web/public/codex-agent-logo.png | Bin 0 -> 41229 bytes .../web/public/codex-app-icon.png | Bin 0 -> 43899 bytes apps/codex-taskboard/web/public/favicon.svg | 7 + apps/codex-taskboard/web/src/App.tsx | 3626 +++++++ apps/codex-taskboard/web/src/actors.ts | 27 + apps/codex-taskboard/web/src/aiChatState.ts | 247 + apps/codex-taskboard/web/src/api.ts | 512 + .../web/src/assets/x-logo-black.png | Bin 0 -> 102327 bytes .../web/src/components/ActorAvatar.tsx | 32 + .../web/src/components/AiChat.tsx | 2648 +++++ .../web/src/components/ArchivedIssues.tsx | 130 + .../web/src/components/BoardColumn.tsx | 365 + .../web/src/components/BoardSettingsMenu.tsx | 133 + .../src/components/ColumnVisibilityMenu.tsx | 126 + .../web/src/components/HiddenColumns.tsx | 107 + .../src/components/InlineMediaComposer.tsx | 275 + .../web/src/components/IssueHierarchyView.tsx | 205 + .../web/src/components/IssueRelations.tsx | 429 + .../web/src/components/LabelPicker.tsx | 128 + .../web/src/components/LinearIcon.tsx | 219 + .../web/src/components/PendingAttachments.tsx | 61 + .../src/components/ProjectAutomationMenu.tsx | 288 + .../web/src/components/TaskCard.tsx | 207 + .../web/src/components/TaskContextMenu.tsx | 418 + .../web/src/components/TaskDetail.tsx | 1316 +++ .../web/src/components/TaskEditor.tsx | 412 + .../web/src/components/TaskFilterMenu.tsx | 625 ++ .../web/src/components/WorkflowBoard.tsx | 1479 +++ .../web/src/components/WorkflowInsertEdge.tsx | 85 + .../web/src/components/WorkflowInspector.tsx | 832 ++ .../web/src/components/WorkflowMark.tsx | 20 + .../web/src/components/WorkflowNode.tsx | 362 + .../web/src/components/WorkflowStepPicker.tsx | 113 + .../web/src/components/workflow.css | 1379 +++ .../web/src/components/workflowCatalog.ts | 934 ++ apps/codex-taskboard/web/src/issueRoute.ts | 22 + apps/codex-taskboard/web/src/labels.ts | 17 + apps/codex-taskboard/web/src/main.tsx | 10 + .../web/src/revisionPolling.mjs | 48 + apps/codex-taskboard/web/src/styles.css | 9459 +++++++++++++++++ apps/codex-taskboard/web/src/taskFilters.ts | 119 + apps/codex-taskboard/web/src/taskHierarchy.ts | 44 + apps/codex-taskboard/web/src/types.ts | 283 + apps/codex-taskboard/web/src/vite-env.d.ts | 1 + apps/codex-taskboard/web/src/workflowStore.ts | 42 + apps/codex-taskboard/web/tsconfig.json | 20 + apps/codex-taskboard/web/vite.config.ts | 20 + apps/codex-taskboard/wrangler.jsonc | 32 + assets/inject/renderer-inject.js | 195 +- crates/codex-plus-core/src/install/mod.rs | 8 +- crates/codex-plus-core/src/launcher.rs | 78 + crates/codex-plus-core/src/lib.rs | 5 + crates/codex-plus-core/src/routes.rs | 125 +- crates/codex-plus-core/src/settings.rs | 5 + crates/codex-plus-core/src/watcher.rs | 23 +- .../src/windows_integration.rs | 11 +- crates/codex-plus-core/tests/bridge_routes.rs | 20 +- crates/codex-plus-core/tests/cdp_bridge.rs | 22 +- crates/codex-plus-core/tests/installers.rs | 9 + crates/codex-plus-core/tests/launcher.rs | 11 +- crates/codex-plus-core/tests/watcher.rs | 56 +- package.json | 7 + 147 files changed, 60438 insertions(+), 44 deletions(-) create mode 100644 apps/codex-taskboard/.gitignore create mode 100644 apps/codex-taskboard/README.md create mode 100644 apps/codex-taskboard/cli/taskctl.mjs create mode 100644 apps/codex-taskboard/cloud/migrations/0001_initial.sql create mode 100644 apps/codex-taskboard/cloud/migrations/0002_project_archive.sql create mode 100644 apps/codex-taskboard/cloud/src/index.mjs create mode 100644 apps/codex-taskboard/docs/cloud-collaboration.md create mode 100644 apps/codex-taskboard/inject/codex-taskboard.user.js create mode 100644 apps/codex-taskboard/package-lock.json create mode 100644 apps/codex-taskboard/package.json create mode 100644 apps/codex-taskboard/scripts/codex-injector-runtime.mjs create mode 100644 apps/codex-taskboard/scripts/codex-injector.mjs create mode 100644 apps/codex-taskboard/scripts/codex-rate-limits.mjs create mode 100644 apps/codex-taskboard/scripts/dev.mjs create mode 100644 apps/codex-taskboard/scripts/migrate-to-cloud.mjs create mode 100644 apps/codex-taskboard/scripts/wrangler-cloud-adapter.mjs create mode 100644 apps/codex-taskboard/server/ai-chat-catalog.mjs create mode 100644 apps/codex-taskboard/server/ai-chat-process.mjs create mode 100644 apps/codex-taskboard/server/ai-chat.mjs create mode 100644 apps/codex-taskboard/server/app.mjs create mode 100644 apps/codex-taskboard/server/cloud-config.mjs create mode 100644 apps/codex-taskboard/server/cloud-proxy.mjs create mode 100644 apps/codex-taskboard/server/database.mjs create mode 100644 apps/codex-taskboard/server/index.mjs create mode 100644 apps/codex-taskboard/shared/domain.mjs create mode 100644 apps/codex-taskboard/shared/taskboard-automation-options.d.mts create mode 100644 apps/codex-taskboard/shared/taskboard-automation-options.mjs create mode 100644 apps/codex-taskboard/shared/taskboard-automation.mjs create mode 100644 apps/codex-taskboard/shared/workflow-control-flow.d.mts create mode 100644 apps/codex-taskboard/shared/workflow-control-flow.mjs create mode 100644 apps/codex-taskboard/shared/workflow-sequence.d.mts create mode 100644 apps/codex-taskboard/shared/workflow-sequence.mjs create mode 100644 apps/codex-taskboard/skills/manage-taskboard/SKILL.md create mode 100644 apps/codex-taskboard/skills/manage-taskboard/agents/openai.yaml create mode 100644 apps/codex-taskboard/skills/manage-taskboard/references/cli.md create mode 100644 apps/codex-taskboard/test/actor-identity.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-api.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-database.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-runner.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-server.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-state.test.mjs create mode 100644 apps/codex-taskboard/test/ai-chat-ui.test.mjs create mode 100644 apps/codex-taskboard/test/board-interactions.test.mjs create mode 100644 apps/codex-taskboard/test/board-views.test.mjs create mode 100644 apps/codex-taskboard/test/cli.test.mjs create mode 100644 apps/codex-taskboard/test/cloud-companion.test.mjs create mode 100644 apps/codex-taskboard/test/cloud-migration.test.mjs create mode 100644 apps/codex-taskboard/test/cloud-revision-polling.test.mjs create mode 100644 apps/codex-taskboard/test/cloud-shared-worker.test.mjs create mode 100644 apps/codex-taskboard/test/comment-activity-style.test.mjs create mode 100644 apps/codex-taskboard/test/helpers/cloud-worker-harness.mjs create mode 100644 apps/codex-taskboard/test/hidden-columns.test.mjs create mode 100644 apps/codex-taskboard/test/icon-system.test.mjs create mode 100644 apps/codex-taskboard/test/inject.test.mjs create mode 100644 apps/codex-taskboard/test/injector-host-runtime.test.mjs create mode 100644 apps/codex-taskboard/test/injector.test.mjs create mode 100644 apps/codex-taskboard/test/input-focus-chrome.test.mjs create mode 100644 apps/codex-taskboard/test/issue-assignee.test.mjs create mode 100644 apps/codex-taskboard/test/issue-detail-inline-focus.test.mjs create mode 100644 apps/codex-taskboard/test/issue-detail-markdown.test.mjs create mode 100644 apps/codex-taskboard/test/issue-detail-route.test.mjs create mode 100644 apps/codex-taskboard/test/issue-relations.test.mjs create mode 100644 apps/codex-taskboard/test/manage-taskboard-skill.test.mjs create mode 100644 apps/codex-taskboard/test/project-automation-settings.test.mjs create mode 100644 apps/codex-taskboard/test/project-home.test.mjs create mode 100644 apps/codex-taskboard/test/server.test.mjs create mode 100644 apps/codex-taskboard/test/taskboard-automation.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-condition-branches.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-condition-layout.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-control-flow-layout.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-control-flow.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-sequence.test.mjs create mode 100644 apps/codex-taskboard/test/workflow-twitter-node.test.mjs create mode 100644 apps/codex-taskboard/test/zapier-workflow.test.mjs create mode 100644 apps/codex-taskboard/web/index.html create mode 100644 apps/codex-taskboard/web/public/Inter-LICENSE.txt create mode 100644 apps/codex-taskboard/web/public/InterVariable.woff2 create mode 100644 apps/codex-taskboard/web/public/codex-agent-logo.png create mode 100644 apps/codex-taskboard/web/public/codex-app-icon.png create mode 100644 apps/codex-taskboard/web/public/favicon.svg create mode 100644 apps/codex-taskboard/web/src/App.tsx create mode 100644 apps/codex-taskboard/web/src/actors.ts create mode 100644 apps/codex-taskboard/web/src/aiChatState.ts create mode 100644 apps/codex-taskboard/web/src/api.ts create mode 100644 apps/codex-taskboard/web/src/assets/x-logo-black.png create mode 100644 apps/codex-taskboard/web/src/components/ActorAvatar.tsx create mode 100644 apps/codex-taskboard/web/src/components/AiChat.tsx create mode 100644 apps/codex-taskboard/web/src/components/ArchivedIssues.tsx create mode 100644 apps/codex-taskboard/web/src/components/BoardColumn.tsx create mode 100644 apps/codex-taskboard/web/src/components/BoardSettingsMenu.tsx create mode 100644 apps/codex-taskboard/web/src/components/ColumnVisibilityMenu.tsx create mode 100644 apps/codex-taskboard/web/src/components/HiddenColumns.tsx create mode 100644 apps/codex-taskboard/web/src/components/InlineMediaComposer.tsx create mode 100644 apps/codex-taskboard/web/src/components/IssueHierarchyView.tsx create mode 100644 apps/codex-taskboard/web/src/components/IssueRelations.tsx create mode 100644 apps/codex-taskboard/web/src/components/LabelPicker.tsx create mode 100644 apps/codex-taskboard/web/src/components/LinearIcon.tsx create mode 100644 apps/codex-taskboard/web/src/components/PendingAttachments.tsx create mode 100644 apps/codex-taskboard/web/src/components/ProjectAutomationMenu.tsx create mode 100644 apps/codex-taskboard/web/src/components/TaskCard.tsx create mode 100644 apps/codex-taskboard/web/src/components/TaskContextMenu.tsx create mode 100644 apps/codex-taskboard/web/src/components/TaskDetail.tsx create mode 100644 apps/codex-taskboard/web/src/components/TaskEditor.tsx create mode 100644 apps/codex-taskboard/web/src/components/TaskFilterMenu.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowBoard.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowInsertEdge.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowInspector.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowMark.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowNode.tsx create mode 100644 apps/codex-taskboard/web/src/components/WorkflowStepPicker.tsx create mode 100644 apps/codex-taskboard/web/src/components/workflow.css create mode 100644 apps/codex-taskboard/web/src/components/workflowCatalog.ts create mode 100644 apps/codex-taskboard/web/src/issueRoute.ts create mode 100644 apps/codex-taskboard/web/src/labels.ts create mode 100644 apps/codex-taskboard/web/src/main.tsx create mode 100644 apps/codex-taskboard/web/src/revisionPolling.mjs create mode 100644 apps/codex-taskboard/web/src/styles.css create mode 100644 apps/codex-taskboard/web/src/taskFilters.ts create mode 100644 apps/codex-taskboard/web/src/taskHierarchy.ts create mode 100644 apps/codex-taskboard/web/src/types.ts create mode 100644 apps/codex-taskboard/web/src/vite-env.d.ts create mode 100644 apps/codex-taskboard/web/src/workflowStore.ts create mode 100644 apps/codex-taskboard/web/tsconfig.json create mode 100644 apps/codex-taskboard/web/vite.config.ts create mode 100644 apps/codex-taskboard/wrangler.jsonc create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 65156b4b1..54940a99e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ apps/codex-plus-manager/src-tauri/gen/ .superpowers/ .claude/ docs/superpowers/ +.codex-index/ +.context/ +.data/ +.idea/ *.log .DS_Store @@ -29,4 +33,6 @@ pnpm-workspace.yaml .trae/ plan.md plan_v2.md +apps/codex-taskboard/.codex-legacy-bridge/ +apps/codex-taskboard/*proof*.png diff --git a/apps/codex-plus-launcher/src/main.rs b/apps/codex-plus-launcher/src/main.rs index af73c5067..e773b3cc1 100644 --- a/apps/codex-plus-launcher/src/main.rs +++ b/apps/codex-plus-launcher/src/main.rs @@ -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)] @@ -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(()) } @@ -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 @@ -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?; @@ -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> { + 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 { + 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 { + 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 { + 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 { + 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(args: I) -> LaunchOptions where I: IntoIterator, @@ -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"); @@ -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"); diff --git a/apps/codex-plus-manager/src-tauri/src/commands.rs b/apps/codex-plus-manager/src-tauri/src/commands.rs index ca3046a2a..1b0ccc7ed 100644 --- a/apps/codex-plus-manager/src-tauri/src/commands.rs +++ b/apps/codex-plus-manager/src-tauri/src/commands.rs @@ -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}; @@ -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, @@ -532,6 +543,7 @@ pub fn launch_codex_plus(request: LaunchRequest) -> CommandResult { #[tauri::command] pub fn restart_codex_plus(request: LaunchRequest) -> CommandResult { 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 已请求重启,启动任务正在后台运行。") } @@ -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 { + 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 { settings_payload("设置已加载。", "设置读取失败") diff --git a/apps/codex-plus-manager/src-tauri/src/lib.rs b/apps/codex-plus-manager/src-tauri/src/lib.rs index da987661a..3d6670ff9 100644 --- a/apps/codex-plus-manager/src-tauri/src/lib.rs +++ b/apps/codex-plus-manager/src-tauri/src/lib.rs @@ -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, diff --git a/apps/codex-plus-manager/src-tauri/tests/windows_subsystem.rs b/apps/codex-plus-manager/src-tauri/tests/windows_subsystem.rs index 7f37e9652..5b0a4e5c4 100644 --- a/apps/codex-plus-manager/src-tauri/tests/windows_subsystem.rs +++ b/apps/codex-plus-manager/src-tauri/tests/windows_subsystem.rs @@ -95,7 +95,7 @@ fn launcher_binary_embeds_codex_icon_resource() { } #[test] -fn windows_binaries_request_administrator_privileges() { +fn windows_binaries_use_standard_user_execution_level() { let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); let manager_build = std::fs::read_to_string(manifest_dir.join("build.rs")).expect("read manager build.rs"); @@ -118,7 +118,7 @@ fn windows_binaries_request_administrator_privileges() { assert!(manager_build.contains("windows-app-manifest.xml")); assert!(launcher_build.contains("windows-app-manifest.xml")); - assert!(windows_manifest.contains("requireAdministrator")); + assert!(windows_manifest.contains("asInvoker")); assert!(windows_manifest.contains("Microsoft.Windows.Common-Controls")); assert!(windows_installer.contains("RequestExecutionLevel admin")); } diff --git a/apps/codex-plus-manager/src-tauri/windows-app-manifest.xml b/apps/codex-plus-manager/src-tauri/windows-app-manifest.xml index 3e50be5a6..f61760bdc 100644 --- a/apps/codex-plus-manager/src-tauri/windows-app-manifest.xml +++ b/apps/codex-plus-manager/src-tauri/windows-app-manifest.xml @@ -15,7 +15,7 @@ - + diff --git a/apps/codex-plus-manager/src/App.tsx b/apps/codex-plus-manager/src/App.tsx index dec74d38e..89c51dad5 100644 --- a/apps/codex-plus-manager/src/App.tsx +++ b/apps/codex-plus-manager/src/App.tsx @@ -32,6 +32,7 @@ import { GripVertical, Info, ImagePlus, + Kanban, Github, ExternalLink, Hammer, @@ -192,6 +193,7 @@ type BackendSettings = { providerSyncLastSelectedProvider: string; relayProfilesEnabled: boolean; enhancementsEnabled: boolean; + codexTaskboardEnabled: boolean; computerUseGuardEnabled: boolean; codexAppPluginMarketplaceUnlock: boolean; codexAppPluginAutoExpand: boolean; @@ -328,6 +330,7 @@ type RelayMode = "official" | "mixedApi" | "pureApi" | "aggregate"; const PROTOCOL_PROXY_BASE_URL = "http://127.0.0.1:57321/v1"; const CHAT_UPSTREAM_BASE_URL_KEY = "codex_plus_chat_base_url"; const SCRIPT_MARKET_REPOSITORY_URL = "https://github.com/BigPizzaV3/CodexPlusPlusScriptMarket"; +const TASKBOARD_PANEL_URL = "http://127.0.0.1:47823/?host=codex"; const emptyContextSelection = (): RelayContextSelection => ({ mcpServers: [], @@ -763,7 +766,13 @@ type StartupResult = CommandResult<{ showUpdate: boolean; }>; -type Route = "overview" | "relay" | "relayEnvironment" | "sessions" | "context" | "enhance" | "dreamSkin" | "zedRemote" | "userScripts" | "recommendations" | "maintenance" | "about" | "settings"; +type TaskboardServiceResult = CommandResult<{ + url: string; + alreadyRunning: boolean; + launched: boolean; +}>; + +type Route = "overview" | "relay" | "relayEnvironment" | "sessions" | "context" | "enhance" | "dreamSkin" | "taskboard" | "zedRemote" | "userScripts" | "recommendations" | "maintenance" | "about" | "settings"; type Theme = "dark" | "light"; const routes: Array<{ id: Route; label: string; icon: LucideIcon; badge?: string }> = [ @@ -773,6 +782,7 @@ const routes: Array<{ id: Route; label: string; icon: LucideIcon; badge?: string { id: "context", label: t("工具与插件"), icon: Network }, { id: "enhance", label: t("Codex增强"), icon: Hammer }, { id: "dreamSkin", label: t("皮肤管理"), icon: Palette }, + { id: "taskboard", label: "Taskboard", icon: Kanban }, { id: "zedRemote", label: t("Zed 远程项目"), icon: ExternalLink }, { id: "userScripts", label: t("脚本市场"), icon: FileCode2 }, { id: "recommendations", label: t("推荐内容"), icon: ExternalLink }, @@ -791,6 +801,7 @@ const defaultSettings: BackendSettings = { providerSyncLastSelectedProvider: "", relayProfilesEnabled: true, enhancementsEnabled: true, + codexTaskboardEnabled: false, computerUseGuardEnabled: false, codexAppPluginMarketplaceUnlock: true, codexAppPluginAutoExpand: true, @@ -877,6 +888,7 @@ const defaultSettings: BackendSettings = { export function App() { const [theme, setTheme] = useState(() => loadInitialTheme()); const [route, setRoute] = useState(() => loadInitialRoute()); + const [taskboardFrameVersion, setTaskboardFrameVersion] = useState(0); const [notice, setNotice] = useState<{ title: string; message: string; status?: Status } | null>(null); const [confirmDialog, setConfirmDialog] = useState<{ title: string; @@ -2588,7 +2600,13 @@ export function App() { const actions = useMemo( () => ({ - refreshCurrent: () => navigate(route), + refreshCurrent: async () => { + if (route === "taskboard") { + setTaskboardFrameVersion((version) => version + 1); + return; + } + await navigate(route); + }, launch, restart, repairPluginMarketplace, @@ -2788,6 +2806,7 @@ export function App() { [route, launchForm, settingsForm, settings, overview, removeOwnedData, update, updateInstallProgress.active, logs, diagnostics, theme, relayFiles, localSessions, zedRemoteProjects, selectedProviderSyncTarget, envConflicts, relayEnvironment, ccsProviders, dreamSkinLibrary, dreamSkinMarket, dreamSkinCommunity, selectedDreamSkinTheme, savedDreamSkinThemeDraft, dreamSkinThemeDraft, dreamSkinDraftDirty, pendingDreamSkinRestart], ); const hasUpdate = update?.updateAvailable === true; + const taskboardEnabled = settingsForm.enhancementsEnabled && settingsForm.codexTaskboardEnabled; return (
@@ -2814,7 +2833,7 @@ export function App() {