From 82b86576098750289c803a0dee6408808d161ff8 Mon Sep 17 00:00:00 2001 From: pavelhov Date: Wed, 12 Aug 2026 14:19:09 -0400 Subject: [PATCH] fix(macos): route Codex on app launch --- README.md | 15 ++++++---- .../MenuBarCore/ActionCoordinator.swift | 19 ++++++++++++ .../MenuBarCoreTests/ActionSuite.swift | 29 +++++++++++++++++++ app/Sources/MenuBarUI/AppDelegate.swift | 12 ++++---- .../src/content/docs/guides/macos-menu-bar.md | 26 +++++++++++------ .../content/docs/ja/guides/macos-menu-bar.md | 14 +++++++-- .../content/docs/ko/guides/macos-menu-bar.md | 13 +++++++-- .../content/docs/ru/guides/macos-menu-bar.md | 17 +++++++---- .../docs/zh-cn/guides/macos-menu-bar.md | 12 ++++++-- src/cli/proxy-lifecycle.ts | 9 +++--- structure/01_runtime.md | 15 ++++++---- structure/05_gui-and-management-api.md | 12 ++++++-- tests/proxy-lifecycle.test.ts | 20 ++++++++++--- 13 files changed, 162 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index c23cb604a1..08107c5beb 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,9 @@ This preview requires macOS 13 or later. It is ad-hoc signed and not notarized y | **Quit Menu Bar** | Closes only the menu app; the proxy and current route keep running. | | **Stop CodexCommander and Quit…** | Restores native Codex, stops the proxy and service, then quits the menu app. | +Here, **Restore Native** means removing CodexCommander-owned routing. A user-managed external Codex +provider is left unchanged. + Route changes show a spinner, elapsed time, and the real **Changing route → Confirming route** phases. After a confirmed route change, quit ChatGPT completely, reopen it, and start a new task. @@ -118,11 +121,13 @@ Codex tasks, history, or authentication, and it does not require a repair comman database. Generated catalogs and caches may remain on disk, but native Codex no longer references them. -On its first launch, the app enables **Launch at Login** so the -menu icon returns after sign-in. The startup row exposes the actual mode: **Desktop** launches the -menu app, **Headless** leaves only an installed background service at login, and **Off** starts -neither automatically. Rebuilt source apps refresh their login registration in place; they are -never copied into Application Support. Full +On its first launch, the app enables **Launch at Login** so the menu icon returns after sign-in. +On every new manual or Login Item launch, the app performs an explicit **Start**: it starts or +attaches to the proxy, then routes managed Codex through it. An external user-managed Codex provider +is preserved. The startup row exposes the actual mode: **Desktop** +performs this app-managed start, **Headless** leaves only an installed background service at login, +and **Off** starts neither automatically. Rebuilt source apps refresh their login registration in +place; they are never copied into Application Support. Full setup, Gatekeeper, release packaging, and troubleshooting details are in the [macOS menu bar guide](docs-site/src/content/docs/guides/macos-menu-bar.md). diff --git a/app/Sources/MenuBarCore/ActionCoordinator.swift b/app/Sources/MenuBarCore/ActionCoordinator.swift index ef701678cb..3def9036a2 100644 --- a/app/Sources/MenuBarCore/ActionCoordinator.swift +++ b/app/Sources/MenuBarCore/ActionCoordinator.swift @@ -37,6 +37,25 @@ public enum CodexCatalogApplyOutcome: Equatable, Sendable { case failed(String) } +public let companionPassiveLaunchArgument = "--ccx-passive-launch" + +/// A direct or Login Item launch is an explicit product Start. A CLI that only +/// ensures the shared proxy passes the fixed passive marker so opening the menu +/// surface cannot override an intentional Native route. +public enum CompanionLaunchPolicy { + public static func run( + using actions: ActionCoordinator?, + arguments: [String] = CommandLine.arguments + ) async -> ProxyControlOutcome { + guard let actions else { return .failed("Lifecycle control is unavailable.") } + return if arguments.dropFirst().contains(companionPassiveLaunchArgument) { + await actions.ensure() + } else { + await actions.start() + } + } +} + /// Executes confirm-gated lifecycle actions through the fixed structured helper. public actor ActionCoordinator { private let lifecycle: any LifecycleCommandRunning diff --git a/app/Sources/MenuBarCoreTests/ActionSuite.swift b/app/Sources/MenuBarCoreTests/ActionSuite.swift index 66f6d05f44..e7c22deae7 100644 --- a/app/Sources/MenuBarCoreTests/ActionSuite.swift +++ b/app/Sources/MenuBarCoreTests/ActionSuite.swift @@ -85,6 +85,35 @@ enum ActionSuite { ) } + t.test("lifecycle: direct companion launch starts routing; passive CLI launch only ensures") { + let lifecycle = FakeLifecycleRunner(results: [ + LifecycleCommandResult( + action: .start, ok: true, state: .running, + changed: true, pid: 41, port: 10100, message: "running" + ), + LifecycleCommandResult( + action: .ensure, ok: true, state: .running, + changed: false, pid: 41, port: 10100, message: "running" + ), + ]) + let coordinator = ActionCoordinator(lifecycle: lifecycle) + t.equal( + sync { await CompanionLaunchPolicy.run( + using: coordinator, + arguments: ["CodexCommanderMenuBar"] + ) }, + .running + ) + t.equal( + sync { await CompanionLaunchPolicy.run( + using: coordinator, + arguments: ["CodexCommanderMenuBar", companionPassiveLaunchArgument] + ) }, + .running + ) + t.equal(sync { await lifecycle.recordedActions() }, [.start, .ensure]) + } + t.test("lifecycle: a helper refusal is surfaced without changing its message") { let lifecycle = FakeLifecycleRunner(results: [ LifecycleCommandResult( diff --git a/app/Sources/MenuBarUI/AppDelegate.swift b/app/Sources/MenuBarUI/AppDelegate.swift index 922663fbbf..4e9c38e62c 100644 --- a/app/Sources/MenuBarUI/AppDelegate.swift +++ b/app/Sources/MenuBarUI/AppDelegate.swift @@ -164,7 +164,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValid ) ) startCompanionHeartbeat() - ensureProxyOnLaunch() + startProxyOnLaunch() } public func applicationDidBecomeActive(_ notification: Notification) { @@ -378,16 +378,16 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValid return true } - /// Finder launch is the app-level start contract. It uses the fixed TS helper and - /// keeps the menu app alive even when startup fails, so Start remains available. - private func ensureProxyOnLaunch() { + /// Manual and Launch-at-Login openings share the explicit Start contract. A failed + /// start leaves the menu app alive so its diagnostics and Start control remain usable. + private func startProxyOnLaunch() { guard !lifecycleInFlight, !restartInFlight, !catalogActionInFlight else { return } lifecycleInFlight = true updateApplicationMenu() controller.setLifecycleControlsEnabled(false) refreshCatalogApplyAvailability() Task { [actions, coordinator] in - let outcome = await actions?.ensure() ?? .failed("Lifecycle control is unavailable.") + let outcome = await CompanionLaunchPolicy.run(using: actions) await coordinator?.forceRefresh() await MainActor.run { [weak self] in guard let self else { return } @@ -399,7 +399,7 @@ public final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuItemValid self.companionHeartbeat?.reportNow() case .catalogUpdateReady(let count): // The proxy is running with a pending catalog refresh; report now so - // a failed pre-ensure report is retried right after startup. + // a failed pre-start report is retried right after startup. self.companionHeartbeat?.reportNow() self.presentCatalogUpdate(staleWorkerCount: count) case .stopped: diff --git a/docs-site/src/content/docs/guides/macos-menu-bar.md b/docs-site/src/content/docs/guides/macos-menu-bar.md index 1569d8d63a..217c3c4fb2 100644 --- a/docs-site/src/content/docs/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/guides/macos-menu-bar.md @@ -24,12 +24,17 @@ To build from source instead, follow [Build from source](#build-from-source) bel The panel has one **Launch at Login** switch and reports the resulting mode: -- **Desktop** — the CodexCommander menu app launches when you sign in and ensures or attaches to exactly - one server. This is the default desktop experience and is reported as **App-managed** in Startup. +- **Desktop** — the CodexCommander menu app launches when you sign in, performs an explicit Start, + starts or attaches to exactly one proxy, and routes managed Codex through it. An external + user-managed Codex provider is preserved. This is the default desktop experience and is reported + as **App-managed** in Startup. - **Headless** — the menu app is not a login item, but an independently installed `ccx service` continues starting and supervising the server. -- **Off** — neither the menu app nor a background service starts automatically; open the app or run - `ccx start` manually. +- **Off** — neither the menu app nor a background service starts automatically. A new manual app + launch performs the same explicit Start-and-route transition as `ccx start`. + +Throughout this guide, **restore native** means removing CodexCommander-owned routing. An external +user-managed Codex provider is left unchanged. The visible app and background server remain separate internally. With the CodexCommander panel active, **Quit Menu Bar** (`⌘Q`) closes only the companion UI and deliberately leaves routing active. @@ -40,8 +45,9 @@ therefore list CodexCommander under both **Open at Login** and **Allow in the Ba responsibilities of one installation, not duplicate app copies. Turning off Launch at Login never installs, removes, starts, or stops the background service. -App-managed startup and the background service solve different problems. The app starts the proxy at -sign-in, which is enough for normal desktop use. The optional background service additionally +App-managed startup and the background service solve different problems. The app starts or attaches +to the proxy at sign-in and routes managed Codex through it, which is enough for normal desktop use. The +optional background service additionally supervises the proxy and restarts it after a crash, so the dashboard labels it **Background recovery** instead of presenting it as a requirement. The companion periodically reports its current Launch at Login state to the local proxy; that short-lived report is kept only in memory and is used @@ -217,9 +223,11 @@ open dist/macos/CodexCommander.app The development app is exactly `dist/macos/CodexCommander.app`. Every build embeds the Bun runtime and CodexCommander server resources inside the app bundle; the running app never executes `src/` from the -checkout. Rebuild the app to pick up source changes. Double-clicking it attempts to ensure the proxy, -but an offline failure or failed start does not close the app: its status panel remains available and -**Start** can be retried. This source workflow does not install or copy the app into Application +checkout. Rebuild the app to pick up source changes. Double-clicking it to launch a new app process +performs an explicit Start: it starts or attaches to the proxy and routes managed Codex through it. +An external user-managed provider is preserved. An offline failure or failed start +does not close the app: its status panel remains available and **Start** can be retried. This source +workflow does not install or copy the app into Application Support. A rebuild at the same path is detected on the next launch and refreshes the existing Login Item registration only when Launch at Login remains on. Each build stamps its exact Git revision into `CodexCommanderSourceRevision` in the bundle's `Info.plist` diff --git a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md index 1096547abc..57f915fe63 100644 --- a/docs-site/src/content/docs/ja/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ja/guides/macos-menu-bar.md @@ -20,9 +20,15 @@ Intel Mac と Apple シリコンの両方に対応する **v0.1.0 ユニバー ## 起動モード -- **Desktop** — サインイン時にメニューアプリを開き、1 つのサーバーを確認または起動します。 +- **Desktop** — サインイン時にメニューアプリが開き、明示的な Start を実行してプロキシを + 起動または接続し、管理対象の Codex をそのプロキシ経由にルーティングします。外部のユーザー管理 + プロバイダーは保持されます。 - **Headless** — メニューアプリは開かず、別途インストールした `ccx service` だけを起動します。 -- **Off** — 自動起動せず、アプリまたは `ccx start` で手動起動します。 +- **Off** — 自動起動しません。アプリを手動で新規起動すると明示的な Start が実行され、管理対象の + Codex がプロキシ経由にルーティングされます。`ccx start` でも同じ処理を実行できます。 + +このガイドで **ネイティブに復元** とは、CodexCommander が所有するルーティングだけを削除することを +意味します。外部のユーザー管理 Codex プロバイダーは変更されません。 設定行から **Launch at Login** を切り替えられます。承認が必要な場合は macOS の Login Items 設定を直接開けます。この切り替えはバックグラウンドサービスをインストール、停止、削除しません。 @@ -184,7 +190,9 @@ open dist/macos/CodexCommander.app 開発アプリの場所は `dist/macos/CodexCommander.app` です。各ビルドで Bun ランタイムと CodexCommander サーバーリソースがアプリバンドルに埋め込まれ、実行中のアプリが checkout の `src/` を 直接実行することはありません。ソース変更を反映するには再ビルドしてください。ダブルクリックすると -プロキシの起動を試みますが、オフラインまたは起動失敗でもアプリは閉じず、パネルと **Start** +新しいアプリプロセスをダブルクリックで起動すると、明示的な Start を実行し、プロキシを起動または +接続して管理対象の Codex をそのプロキシ経由にルーティングします。外部のユーザー管理プロバイダーは +保持されます。オフラインまたは起動失敗でもアプリは閉じず、パネルと **Start** コントロールは利用できます。開発中はこの場所に置き、Application Support へコピーしないでください。 各ビルドは正確な Git リビジョンをバンドルの `Info.plist` の `CodexCommanderSourceRevision` に記録し、 ビルド完了時にも表示します。未コミットのソースには `-dirty` が付くため、最終配布ビルドの前に diff --git a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md index a13a75faa0..adb0edd8de 100644 --- a/docs-site/src/content/docs/ko/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ko/guides/macos-menu-bar.md @@ -20,9 +20,14 @@ Intel Mac과 Apple Silicon을 모두 지원하는 **v0.1.0 유니버설 미리 ## 시작 모드 -- **Desktop** — 로그인할 때 메뉴 앱을 열고 정확히 하나의 서버에 연결하거나 시작합니다. +- **Desktop** — 로그인할 때 메뉴 앱이 열리고 명시적 Start를 실행해 프록시를 시작하거나 연결한 다음 + 관리되는 Codex를 해당 프록시를 통하도록 라우팅합니다. 외부 사용자 관리 공급자는 그대로 유지됩니다. - **Headless** — 메뉴 앱 없이 별도로 설치한 `ccx service`만 시작합니다. -- **Off** — 자동으로 시작하지 않으며 앱 또는 `ccx start`로 수동 시작합니다. +- **Off** — 자동으로 시작하지 않습니다. 앱을 수동으로 새로 실행하면 명시적 Start가 실행되어 관리되는 + Codex가 프록시를 통하도록 라우팅됩니다. `ccx start`도 같은 전환을 실행합니다. + +이 가이드에서 **네이티브 복원**은 CodexCommander가 소유한 라우팅만 제거한다는 뜻입니다. 외부 사용자 +관리 Codex 공급자는 변경되지 않습니다. 시작 행에서 **Launch at Login**을 변경할 수 있습니다. 승인이 필요하면 macOS Login Items 설정을 직접 엽니다. 이 스위치는 백그라운드 서비스를 설치, 중지 또는 제거하지 않습니다. @@ -178,7 +183,9 @@ open dist/macos/CodexCommander.app 개발 앱의 위치는 정확히 `dist/macos/CodexCommander.app`입니다. 빌드할 때마다 Bun 런타임과 CodexCommander 서버 리소스가 앱 번들에 포함되며, 실행 중인 앱은 체크아웃의 `src/`를 직접 실행하지 않습니다. 소스 변경을 반영하려면 앱을 다시 빌드하세요. 개발 중에는 이 위치에 두고 Application Support로 -복사하지 마세요. 더블클릭하면 프록시 시작을 시도하지만 오프라인이거나 시작에 실패해도 앱은 닫히지 +복사하지 마세요. 더블클릭으로 새 앱 프로세스를 실행하면 명시적 Start를 실행해 프록시를 시작하거나 +연결하고 관리되는 Codex를 해당 프록시를 통하도록 라우팅합니다. 외부 사용자 관리 공급자는 그대로 +유지됩니다. 오프라인이거나 시작에 실패해도 앱은 닫히지 않으며 패널과 **Start** 컨트롤을 계속 사용할 수 있습니다. 각 빌드는 정확한 Git 리비전을 번들의 `Info.plist`에 있는 `CodexCommanderSourceRevision`에 기록하고 빌드 마지막에도 출력합니다. 커밋하지 않은 소스에는 `-dirty`가 붙으므로 최종 번들을 만들기 전에 diff --git a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md index 5642885be1..acf505b830 100644 --- a/docs-site/src/content/docs/ru/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/ru/guides/macos-menu-bar.md @@ -20,10 +20,15 @@ description: Установка и использование нативного ## Режимы запуска -- **Desktop** — приложение меню открывается при входе и подключается ровно к одному серверу или - запускает его. +- **Desktop** — приложение меню открывается при входе, выполняет явный Start, запускает прокси или + подключается к нему и направляет управляемый Codex через этот прокси. Внешний провайдер, + настроенный пользователем, сохраняется. - **Headless** — запускается только отдельно установленная служба `ccx service`, без приложения меню. -- **Off** — автозапуск выключен; используйте приложение или `ccx start` вручную. +- **Off** — автозапуск выключен. При новом ручном запуске приложение выполняет явный Start и + направляет управляемый Codex через прокси; `ccx start` выполняет тот же переход. + +В этом руководстве **восстановить нативную маршрутизацию** означает удалить только маршрутизацию, +которой владеет CodexCommander. Внешний провайдер Codex, настроенный пользователем, не изменяется. Переключатель **Launch at Login** находится в строке запуска. Если требуется разрешение, приложение открывает настройки Login Items macOS. Переключатель не устанавливает, не останавливает и не удаляет @@ -201,8 +206,10 @@ open dist/macos/CodexCommander.app Приложение для разработки находится ровно в `dist/macos/CodexCommander.app`. Каждая сборка встраивает среду Bun и ресурсы сервера CodexCommander в пакет приложения; запущенное приложение не выполняет `src/` непосредственно из checkout. Чтобы применить изменения исходников, пересоберите приложение. -Оставляйте его там во время разработки и не копируйте в Application Support. Двойной щелчок пытается -запустить прокси, но при сбое или работе офлайн не закрывает приложение: панель и кнопка **Start** +Оставляйте его там во время разработки и не копируйте в Application Support. При двойном щелчке, +который запускает новый процесс приложения, оно выполняет явный Start, запускает прокси или +подключается к нему и направляет управляемый Codex через него; внешний провайдер пользователя сохраняется. +При сбое или работе офлайн приложение не закрывается: панель и кнопка **Start** остаются доступными. Каждая сборка записывает точную ревизию Git в `CodexCommanderSourceRevision` файла `Info.plist` внутри пакета и печатает её после сборки. Для незакоммиченного исходного кода добавляется `-dirty`, поэтому diff --git a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md index 2adec05a6e..d02cb287e3 100644 --- a/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md +++ b/docs-site/src/content/docs/zh-cn/guides/macos-menu-bar.md @@ -20,9 +20,14 @@ macOS 伴侣会在菜单栏中显示最有用的 CodexCommander 状态,同时 ## 启动模式 -- **Desktop** — 登录时打开菜单栏应用,并连接或启动唯一一个服务器。 +- **Desktop** — 登录时打开菜单栏应用,执行显式 Start,启动或连接代理,并让托管的 Codex 经由该代理 + 路由。用户管理的外部提供商会保持不变。 - **Headless** — 不打开菜单栏应用,只启动另行安装的 `ccx service`。 -- **Off** — 不自动启动;手动打开应用或运行 `ccx start`。 +- **Off** — 不自动启动;手动新启动应用会执行显式 Start 并让托管的 Codex 经由代理路由,运行 + `ccx start` 会执行同一转换。 + +在本指南中,**恢复原生路由**是指仅移除 CodexCommander 管理的路由。用户管理的外部 Codex 提供商 +不会被更改。 可在启动行切换 **Launch at Login**。如果需要批准,应用会直接打开 macOS Login Items 设置。 此开关不会安装、停止或删除后台服务。 @@ -155,7 +160,8 @@ open dist/macos/CodexCommander.app 开发应用的唯一位置是 `dist/macos/CodexCommander.app`。每次构建都会把 Bun 运行时和 CodexCommander 服务器资源嵌入应用包;运行中的应用不会直接执行检出目录里的 `src/`。源代码发生变化后请重新构建 -应用。开发期间请保留在此位置,不要复制到 Application Support。双击会尝试确保代理运行;即使离线 +应用。开发期间请保留在此位置,不要复制到 Application Support。双击启动新的应用进程时会执行显式 +Start,启动或连接代理,并让托管的 Codex 经由该代理路由;用户管理的外部提供商会保持不变。即使离线 或启动失败,应用也不会关闭,面板和 **Start** 控件仍可使用。 每次构建都会把准确的 Git 修订写入应用包 `Info.plist` 的 `CodexCommanderSourceRevision`,并在构建结束时 输出。未提交的源码会带有 `-dirty`,因此制作最终包前请先提交。 diff --git a/src/cli/proxy-lifecycle.ts b/src/cli/proxy-lifecycle.ts index 3605916d50..d6c4ebaf17 100644 --- a/src/cli/proxy-lifecycle.ts +++ b/src/cli/proxy-lifecycle.ts @@ -414,6 +414,7 @@ export async function waitForProxyReadiness( /** Fixed LaunchServices argv for the repo-built companion (or a registered release app). */ const MACOS_COMPANION_BUNDLE_ID = "com.codexcommander.menubar"; +export const MACOS_COMPANION_PASSIVE_LAUNCH_ARG = "--ccx-passive-launch"; export function macOSCompanionOpenArguments( options: { @@ -434,12 +435,12 @@ export function macOSCompanionOpenArguments( const exists = options.exists ?? existsSync; if (options.appPath) { return exists(options.appPath) - ? ["-g", options.appPath] - : ["-g", "-b", MACOS_COMPANION_BUNDLE_ID]; + ? ["-g", options.appPath, "--args", MACOS_COMPANION_PASSIVE_LAUNCH_ARG] + : ["-g", "-b", MACOS_COMPANION_BUNDLE_ID, "--args", MACOS_COMPANION_PASSIVE_LAUNCH_ARG]; } const app = join(repoRoot, "dist", "macos", "CodexCommander.app"); - if (exists(app)) return ["-g", app]; - return ["-g", "-b", MACOS_COMPANION_BUNDLE_ID]; + if (exists(app)) return ["-g", app, "--args", MACOS_COMPANION_PASSIVE_LAUNCH_ARG]; + return ["-g", "-b", MACOS_COMPANION_BUNDLE_ID, "--args", MACOS_COMPANION_PASSIVE_LAUNCH_ARG]; } /** Best-effort source/release companion launch. Never starts from service children. */ diff --git a/structure/01_runtime.md b/structure/01_runtime.md index 66b1ed1b03..5d02893a08 100644 --- a/structure/01_runtime.md +++ b/structure/01_runtime.md @@ -47,12 +47,15 @@ their own files. ## Lifecycle -Explicit starts (`ccx start`, companion Start, and service create/`install`/`repair`/`start`) enable -Codex integration, refuse a duplicate PID, start the proxy, write +Explicit starts (`ccx start`, every new companion launch, companion Start, and service +create/`install`/`repair`/`start`) enable managed Codex integration, preserve an external user-managed +provider, refuse a duplicate PID, start the proxy, write `~/.codexcommander/codexcommander.pid`, and sync Codex config/catalog. Automatic `ensure` preserves an intentional OFF state. Normal standalone shutdown restores native routing. Service mode sets `CCX_SERVICE=1`, so manager restarts preserve the current route; explicit service stop and uninstall restore and verify native routing before terminating anything. +In this document, restoring native means removing CodexCommander-owned routing; an external +user-managed Codex provider is preserved. An installed Codex shim is checked on ordinary CLI startup with a regular-file/1 MiB state bound plus bounded metadata and prefix reads. A complete replacement must produce identical fingerprints and @@ -93,9 +96,9 @@ untouched. The GUI sidebar stop button calls this endpoint. - 다른 대안 대신 이 방식을 선택한 이유: Absolute dotenv expansion bypasses a relative-path check, global dotenv removal breaks supported configuration, and an environment-only marker can itself come from dotenv. - 장점, 단점 및 영향: Node-launcher starts preserve genuine shell overrides. Direct Bun launches without a provenance signal fail closed for all three ambient Anthropic slots — credentials included, because subscription mode leaves `CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST` unset by design (#253) and a `settings.env` merge can still replace the destination after launch, so a preserved key would travel with it. The cost is that `bun src/cli/index.ts` loses ambient Anthropic values; locally linked or packaged starts through `bin/ccx.mjs` preserve genuine shell exports by proof. Durable artifacts use the running or bundled Bun. -The macOS companion starts with an `ensure` attempt when Finder opens it. A failed or offline start -must leave the menu app alive with its status/Start controls available; it cannot self-terminate just -because the proxy is unavailable. Its **Quit** action terminates only the AppKit process. Explicit +Every new manual or Login Item launch of the macOS companion performs an explicit Start. A failed or +offline start must leave the menu app alive with its status/Start controls available; it cannot +self-terminate just because the proxy is unavailable. Its **Quit** action terminates only the AppKit process. Explicit **Start** enables Codex routing through the proxy. **Stop** restores and verifies native routing before termination and keeps the menu app open. **Restart** runs the canonical stop→start transaction: it restores native routing before terminating the old proxy, then its explicit Start phase launches the @@ -105,7 +108,7 @@ lifecycle. The main app is the default desktop Login Item; launchd remains an independent optional headless server supervisor. Login registration never changes provider, proxy, or service configuration. -The launch `ensure` also synchronizes the Codex model catalog. Long-lived Codex workers that loaded +The launch Start also synchronizes the Codex model catalog. Long-lived Codex workers that loaded an older roster do not make the proxy unhealthy: the companion keeps a persistent **Agent catalog update ready** state and offers the separate, confirmation-gated **Apply agent catalog** action. Applying re-synchronizes the catalog, sends `SIGTERM` only to exact current-user `codex … app-server` diff --git a/structure/05_gui-and-management-api.md b/structure/05_gui-and-management-api.md index ca62aef63c..4a3bc06728 100644 --- a/structure/05_gui-and-management-api.md +++ b/structure/05_gui-and-management-api.md @@ -282,13 +282,19 @@ duplicate secret. The active development build is `/dist/macos/CodexCommander.app`. Every built app runs the Bun runtime and server resources embedded in its own `Contents/Resources/runtime`; it never discovers or executes checkout `src/`, so developers rebuild the app to pick up source changes. The development app -is not copied into Application Support, and no bundle shells through an ambient `ccx`. On launch it runs an -ensure lifecycle action and automatically synchronizes the Codex model catalog, but remains open and -actionable after an offline or startup failure. **Quit** terminates only the AppKit UI. **Stop** and +is not copied into Application Support, and no bundle shells through an ambient `ccx`. Every new +manual or Login Item launch runs the explicit Start lifecycle action: it starts or attaches to the +owned proxy, routes managed Codex through it while preserving an external user-managed provider, and +synchronizes the Codex model catalog. The companion remains +open and actionable after an offline or startup failure. Passive `ensure` remains a separate bridge +operation for catalog rechecks that must not override a Native route selected during the current app +session. **Quit** terminates only the AppKit UI. **Stop** and **Restart** are separate confirmation-gated operations: Stop uses the fixed lifecycle helper to persist OFF, restore and verify native Codex, stop any manager, and leave the menu app open; Restart uses the canonical stop→start transaction and reports success only after replacement identity verification. **Restore Native Codex** changes only routing and deliberately leaves the proxy running. +Here and below, restoring native means removing CodexCommander-owned routing; an external +user-managed Codex provider is preserved. Both explicit route directions confirm the saved routing document through the fresh route endpoint before reporting success. A confirmed route change tells the user to quit ChatGPT completely, reopen it, and start a new task; the companion never presents the existing host as already switched. diff --git a/tests/proxy-lifecycle.test.ts b/tests/proxy-lifecycle.test.ts index 77c9391e86..83ae87de98 100644 --- a/tests/proxy-lifecycle.test.ts +++ b/tests/proxy-lifecycle.test.ts @@ -1206,25 +1206,37 @@ describe("shared proxy lifecycle authority", () => { env: {}, appPath: "/repo/dist/macos/CodexCommander.app", exists: () => true, - })).toEqual(["-g", "/repo/dist/macos/CodexCommander.app"]); + })).toEqual([ + "-g", "/repo/dist/macos/CodexCommander.app", + "--args", "--ccx-passive-launch", + ]); expect(macOSCompanionOpenArguments({ platform: "darwin", env: {}, appPath: "/repo/dist/macos/CodexCommander.app", exists: () => false, - })).toEqual(["-g", "-b", "com.codexcommander.menubar"]); + })).toEqual([ + "-g", "-b", "com.codexcommander.menubar", + "--args", "--ccx-passive-launch", + ]); // Default path prefers the rebranded CodexCommander.app when it exists. expect(macOSCompanionOpenArguments({ platform: "darwin", env: {}, exists: (p) => p.endsWith("/dist/macos/CodexCommander.app"), - })).toEqual(["-g", expect.stringContaining("/dist/macos/CodexCommander.app")]); + })).toEqual([ + "-g", expect.stringContaining("/dist/macos/CodexCommander.app"), + "--args", "--ccx-passive-launch", + ]); // No local build at all: open by the rebranded bundle id. expect(macOSCompanionOpenArguments({ platform: "darwin", env: {}, exists: () => false, - })).toEqual(["-g", "-b", "com.codexcommander.menubar"]); + })).toEqual([ + "-g", "-b", "com.codexcommander.menubar", + "--args", "--ccx-passive-launch", + ]); expect(macOSCompanionOpenArguments({ platform: "darwin", env: { CCX_SERVICE: "1" },