Skip to content
Closed
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
39 changes: 29 additions & 10 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,27 +859,46 @@ impl BridgeRuntimeService for LauncherRuntimeService {
}))
}

async fn open_manager(&self) -> anyhow::Result<Value> {
let target = codex_plus_core::install::spawn_companion(
codex_plus_core::install::MANAGER_BINARY,
std::iter::empty::<&str>(),
)
.map_err(|error| anyhow::anyhow!("启动管理工具失败:{error}"))?;
async fn open_manager(&self, payload: Value) -> anyhow::Result<Value> {
let navigation =
codex_plus_core::manager_navigation::save_pending_manager_navigation_from_payload(
&payload,
)?;
let target = codex_plus_core::install::open_or_activate_manager()
.map_err(|error| anyhow::anyhow!("启动管理工具失败:{error}"))
.map_err(|error| {
codex_plus_core::manager_navigation::rollback_pending_manager_navigation_after_launch_failure(
navigation.as_ref(),
error,
)
})?;
Ok(json!({
"status": "ok",
"path": target
"path": target,
"navigation": navigation
}))
}

async fn open_transient_manager(&self) -> anyhow::Result<Value> {
async fn open_transient_manager(&self, payload: Value) -> anyhow::Result<Value> {
let navigation =
codex_plus_core::manager_navigation::save_pending_manager_navigation_from_payload(
&payload,
)?;
let target = codex_plus_core::install::spawn_companion(
codex_plus_core::install::MANAGER_BINARY,
["--transient"],
)
.map_err(|error| anyhow::anyhow!("启动管理工具失败:{error}"))?;
.map_err(|error| anyhow::anyhow!("启动管理工具失败:{error}"))
.map_err(|error| {
codex_plus_core::manager_navigation::rollback_pending_manager_navigation_after_launch_failure(
navigation.as_ref(),
error,
)
})?;
Ok(json!({
"status": "ok",
"path": target
"path": target,
"navigation": navigation
}))
}

Expand Down
7 changes: 7 additions & 0 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,13 @@ pub fn startup_options() -> CommandResult<StartupPayload> {
)
}

#[tauri::command]
pub fn consume_pending_manager_navigation(
) -> Result<Option<codex_plus_core::manager_navigation::ManagerNavigationIntent>, String> {
codex_plus_core::manager_navigation::consume_pending_manager_navigation()
.map_err(|error| error.to_string())
}

pub fn startup_should_show_update() -> bool {
should_show_update(
std::env::args(),
Expand Down
8 changes: 7 additions & 1 deletion apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::sync::atomic::{AtomicBool, Ordering};

use tauri::menu::{Menu, MenuItem};
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
use tauri::{Manager, WindowEvent};
use tauri::{Emitter, Manager, WindowEvent};

const TRAY_ID: &str = "codex_plus_tray";

Expand All @@ -14,6 +14,7 @@ const TRAY_MENU_SHOW: &str = "tray_show_main";
const TRAY_MENU_DREAM_SKIN_APPLY: &str = "tray_apply_dream_skin";
const TRAY_MENU_QUIT: &str = "tray_quit_app";
const DREAM_SKIN_DEBUG_PORT: u16 = 9229;
const MANAGER_NAVIGATION_EVENT: &str = "manager-navigation-requested";

pub fn run() {
install_panic_logger();
Expand Down Expand Up @@ -65,6 +66,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
commands::backend_version,
commands::startup_options,
commands::consume_pending_manager_navigation,
commands::load_overview,
commands::launch_codex_plus,
commands::restart_codex_plus,
Expand Down Expand Up @@ -271,13 +273,17 @@ fn register_main_window_events<R: tauri::Runtime>(
let minimized_window = event_window.clone();
let close_event_window = event_window.clone();
let close_event_app = event_window.app_handle().clone();
let focus_event_window = event_window.clone();

event_window.on_window_event(move |event| match event {
WindowEvent::Resized(_) => {
if matches!(minimized_window.is_minimized(), Ok(true)) {
let _ = minimized_window.hide();
}
}
WindowEvent::Focused(true) => {
let _ = focus_event_window.emit(MANAGER_NAVIGATION_EVENT, ());
}
WindowEvent::CloseRequested { api, .. } => {
if APP_EXITING.load(Ordering::SeqCst) {
return;
Expand Down
92 changes: 86 additions & 6 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ type BackendSettings = {
codexAppServiceTierControls: boolean;
codexAppPetRealMouseLook: boolean;
codexAppStepwiseEnabled: boolean;
codexAppAnswerOutlineEnabled: boolean;
codexAppStepwiseDirectSend: boolean;
codexAppStepwiseGenerationMode: StepwiseGenerationMode;
codexAppStepwiseBaseUrl: string;
codexAppStepwiseApiKey: string;
codexAppStepwiseApiKeyEnv: string;
Expand Down Expand Up @@ -350,6 +352,7 @@ type CodexContextEntries = {
};

type RelayProtocol = "responses" | "chatCompletions";
type StepwiseGenerationMode = "auto" | "manual";
type RelayMode = "official" | "mixedApi" | "pureApi" | "aggregate";
type RelaySessionProvider = "custom" | "openai";
const CHAT_UPSTREAM_BASE_URL_KEY = "codex_plus_chat_base_url";
Expand Down Expand Up @@ -827,9 +830,17 @@ type StartupResult = CommandResult<{
showUpdate: boolean;
}>;

type ManagerNavigationIntent = {
page: "settings";
section?: "stepwise";
};

type Route = "overview" | "relay" | "relayEnvironment" | "sessions" | "context" | "weixin" | "enhance" | "dreamSkin" | "zedRemote" | "userScripts" | "recommendations" | "maintenance" | "about" | "settings";
type Theme = "dark" | "light";

const MANAGER_NAVIGATION_EVENT = "manager-navigation-requested";
const SETTINGS_STEPWISE_SECTION_ID = "settings-stepwise";

const routes: Array<{ id: Route; label: string; icon: LucideIcon; badge?: string }> = [
{ id: "overview", label: t("概览"), icon: LayoutDashboard },
{ id: "relay", label: t("供应商配置"), icon: KeyRound },
Expand Down Expand Up @@ -892,7 +903,9 @@ const defaultSettings: BackendSettings = {
codexAppServiceTierControls: false,
codexAppPetRealMouseLook: false,
codexAppStepwiseEnabled: false,
codexAppAnswerOutlineEnabled: false,
codexAppStepwiseDirectSend: false,
codexAppStepwiseGenerationMode: "auto",
codexAppStepwiseBaseUrl: "",
codexAppStepwiseApiKey: "",
codexAppStepwiseApiKeyEnv: "CODEX_STEPWISE_API_KEY",
Expand Down Expand Up @@ -966,6 +979,7 @@ const defaultSettings: BackendSettings = {
export function App() {
const [theme, setTheme] = useState<Theme>(() => loadInitialTheme());
const [route, setRoute] = useState<Route>(() => loadInitialRoute());
const [pendingSettingsSection, setPendingSettingsSection] = useState<ManagerNavigationIntent["section"] | null>(null);
const [notice, setNotice] = useState<{ title: string; message: string; status?: Status } | null>(null);
const [confirmDialog, setConfirmDialog] = useState<{
title: string;
Expand Down Expand Up @@ -2700,6 +2714,22 @@ export function App() {
await call<void>("manager_hide_to_tray");
};

const consumePendingManagerNavigation = async (): Promise<boolean> => {
try {
const navigation = await invoke<ManagerNavigationIntent | null>("consume_pending_manager_navigation");
if (!navigation) return false;
if (navigation.page === "settings") {
setPendingSettingsSection(navigation.section ?? null);
setRoute("settings");
await refreshSettings(true);
return true;
}
} catch (error) {
logDiagnostic("manager.navigation_failed", { error: stringifyError(error) });
}
return false;
};

const showResultNotice = (
title: string,
result: Pick<CommandResult<unknown>, "message" | "status">,
Expand All @@ -2712,14 +2742,15 @@ export function App() {
useEffect(() => {
void (async () => {
const startup = await run(() => call<StartupResult>("startup_options"));
if (startup?.showUpdate) {
const handledNavigation = await consumePendingManagerNavigation();
if (!handledNavigation && startup?.showUpdate) {
setRoute("about");
void checkUpdate(false);
} else {
void checkUpdate(true);
}
await refreshOverview(true);
await refreshSettings(true);
if (!handledNavigation) await refreshSettings(true);
await refreshRelay(true);
await refreshEnvConflicts(true);
await refreshProviderSyncTargets(true);
Expand All @@ -2729,6 +2760,39 @@ export function App() {
})();
}, []);

useEffect(() => {
let disposed = false;
let stopListening: (() => void) | undefined;
void listen(MANAGER_NAVIGATION_EVENT, () => {
if (!disposed) void consumePendingManagerNavigation();
}).then((unlisten) => {
if (disposed) unlisten();
else stopListening = unlisten;
});
return () => {
disposed = true;
stopListening?.();
};
}, []);

useEffect(() => {
if (route !== "settings" || pendingSettingsSection !== "stepwise") return;
let secondFrame = 0;
const firstFrame = window.requestAnimationFrame(() => {
secondFrame = window.requestAnimationFrame(() => {
document.getElementById(SETTINGS_STEPWISE_SECTION_ID)?.scrollIntoView({
behavior: "smooth",
block: "start",
});
setPendingSettingsSection(null);
});
});
return () => {
window.cancelAnimationFrame(firstFrame);
if (secondFrame) window.cancelAnimationFrame(secondFrame);
};
}, [pendingSettingsSection, route]);

useEffect(() => {
if (getLanguage() === "en") {
void invoke("update_tray_labels", {
Expand Down Expand Up @@ -4405,9 +4469,9 @@ function EnhanceScreen({
<FeatureToggle title={t("对话居中宽度")} detail={t("把主对话和输入框限制到固定最大宽度,适合大屏阅读。")} checked={form.codexAppConversationView} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppConversationView", value)} />
<FeatureToggle title={t("切换对话保留位置")} detail={t("切换 thread 时恢复上一次浏览位置。")} checked={form.codexAppThreadScrollRestore} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppThreadScrollRestore", value)} />
</FeatureGroup>
<FeatureGroup title="Stepwise" detail={t("基于当前对话生成下一步建议,使用独立 API 配置。")}>
<FeatureToggle title="Stepwise" detail={t("在 Codex 页面显示可拖动的后续建议浮层;建议由单独配置的 Stepwise API 生成。启停后需重启 Codex++ 生效。")} checked={form.codexAppStepwiseEnabled} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppStepwiseEnabled", value)} />
<FeatureToggle title={t("Stepwise 直接发送")} detail={t("点击建议后自动发送;关闭时只填入输入框。")} checked={form.codexAppStepwiseDirectSend} disabled={!masterEnabled || !form.codexAppStepwiseEnabled} onChange={(value) => setEnhanceFlag("codexAppStepwiseDirectSend", value)} />
<FeatureGroup title={t("悬浮球")} detail={t("控制下一步建议与回答大纲。")}>
<FeatureToggle title="Stepwise" detail={t("根据当前回答生成下一步建议。")} checked={form.codexAppStepwiseEnabled} disabled={!masterEnabled} onChange={(value) => setPersistedEnhanceFlag("codexAppStepwiseEnabled", value)} />
<FeatureToggle title={t("回答大纲")} detail={t("整理当前回答的结构。")} checked={form.codexAppAnswerOutlineEnabled} disabled={!masterEnabled} onChange={(value) => setPersistedEnhanceFlag("codexAppAnswerOutlineEnabled", value)} />
</FeatureGroup>
<FeatureGroup title={t("界面与启动")} detail={t("控制语言、启动速度和 Codex 原生界面调整。")}>
{isWindowsPlatform ? <FeatureToggle title={t("桌宠跟随真实鼠标")} detail={t("仅支持 V2 桌宠;不会修改宠物文件。将 V2 的 Computer Use 光标朝向动作映射到真实鼠标,V1 开启后安全不生效;拖拽、原生悬停或 Computer Use 活跃时自动让步。")} checked={form.codexAppPetRealMouseLook} disabled={!masterEnabled} onChange={(value) => setPersistedEnhanceFlag("codexAppPetRealMouseLook", value)} /> : null}
Expand Down Expand Up @@ -6204,7 +6268,7 @@ function SettingsScreen({
placeholder={t("例如 gpt-5.4-mini")}
/>
</Field>
<div className="settings-block stepwise-settings-block">
<div className="settings-block stepwise-settings-block" id={SETTINGS_STEPWISE_SECTION_ID}>
<div className="section-title">Stepwise</div>
<div className="stepwise-settings-section">{t("连接")}</div>
<div className="form-row">
Expand All @@ -6223,6 +6287,16 @@ function SettingsScreen({
/>
</Field>
</div>
<Field label={t("生成模式")}>
<AppSelect
value={form.codexAppStepwiseGenerationMode}
onChange={(value) => onFormChange({ ...form, codexAppStepwiseGenerationMode: value as StepwiseGenerationMode })}
options={[
{ value: "auto", label: t("自动生成") },
{ value: "manual", label: t("手动刷新") },
]}
/>
</Field>
<Field label="API Key">
<Input
type="password"
Expand Down Expand Up @@ -9300,6 +9374,8 @@ function normalizeSettings(settings: BackendSettings): BackendSettings {
codexAppDreamSkinPaused: settings.codexAppDreamSkinPaused === true,
codexAppDreamSkinThemeConfig: normalizeDreamSkinTheme(settings.codexAppDreamSkinThemeConfig),
codexAppDreamSkinImagePath: (settings.codexAppDreamSkinImagePath || "").trim(),
codexAppAnswerOutlineEnabled: settings.codexAppAnswerOutlineEnabled === true,
codexAppStepwiseGenerationMode: normalizeStepwiseGenerationMode(settings.codexAppStepwiseGenerationMode),
codexAppStepwiseMaxItems: clampNumber(settings.codexAppStepwiseMaxItems ?? 6, 0, 6),
codexAppStepwiseMaxInputChars: clampNumber(settings.codexAppStepwiseMaxInputChars || 6000, 1000, 24000),
codexAppStepwiseMaxOutputTokens: clampNumber(settings.codexAppStepwiseMaxOutputTokens || 500, 100, 4000),
Expand All @@ -9320,6 +9396,10 @@ function clampNumber(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, Math.round(value)));
}

function normalizeStepwiseGenerationMode(value: StepwiseGenerationMode | undefined): StepwiseGenerationMode {
return value === "manual" ? "manual" : "auto";
}

function parsePort(value: string, fallback: number): number {
const parsed = Number.parseInt(value, 10);
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535 ? parsed : fallback;
Expand Down
16 changes: 16 additions & 0 deletions apps/codex-plus-manager/src/renderer-inject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ describe("renderer injection header compatibility", () => {
assert.match(renderer, /codexPlusIsNodeTestHarness/);
});

it("keeps the floating-panel feature flags opt-in", async () => {
const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8");

assert.match(renderer, /stepwise: false, answerOutline: false/);
assert.match(renderer, /answerOutline: "codexAppAnswerOutlineEnabled"/);
assert.match(renderer, /data-codex-plus-setting="answerOutline"/);
});

it("syncs the outline toggle through the existing Stepwise panel bridge", async () => {
const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8");

assert.match(renderer, /answerOutlineEnabled = codexPlusSettings\(\)\.answerOutline/);
assert.match(renderer, /answerOutlineEnabled: !!answerOutlineEnabled/);
assert.match(renderer, /key === "answerOutline"/);
});

it("initializes renderer styles without unresolved template identifiers", async () => {
const renderer = await readFile(new URL("../../../assets/inject/renderer-inject.js", import.meta.url), "utf8");

Expand Down
Loading
Loading