diff --git a/cmd/launch/codex_app.go b/cmd/launch/codex_app.go index 0f37bb0840..e711203e4a 100644 --- a/cmd/launch/codex_app.go +++ b/cmd/launch/codex_app.go @@ -31,6 +31,7 @@ const ( codexAppIntegrationName = "codex-app" codexAppProfileName = "ollama-launch-codex-app" codexAppBundleID = "com.openai.codex" + codexAppLaunchURL = "codex://threads/new?mode=codex" codexAppModelCatalogFilename = proxy.CodexDesktopModelCatalogFilename codexAppRoutingCatalogFilename = proxy.CodexDesktopRoutingCatalogFilename codexAppAutoReviewModelEnv = "OLLAMA_CODEX_AUTO_REVIEW_MODEL" @@ -2078,13 +2079,7 @@ func defaultCodexAppOpenApp(args []string) error { } return fmt.Errorf("ChatGPT was not found; install it from https://chatgpt.com/download, then re-run 'ollama launch chatgpt'") case "darwin": - if path := codexAppAppPath(); path != "" { - cmd := exec.Command("open", path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() - } - cmd := exec.Command("open", "-b", codexAppBundleID) + cmd := exec.Command("open", codexAppDarwinOpenArgs(codexAppAppPath())...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() @@ -2093,12 +2088,26 @@ func defaultCodexAppOpenApp(args []string) error { } } +func codexAppDarwinOpenArgs(path string) []string { + args := []string{"-b", codexAppBundleID} + if path != "" { + args = []string{path} + } + if (&CodexApp{}).OllamaConfigured() { + if path != "" { + args = []string{"-a", path} + } + args = append(args, codexAppLaunchURL) + } + return args +} + func defaultCodexAppOpenAppPath(path string) error { switch codexAppGOOS { case "windows": return exec.Command("powershell.exe", "-NoProfile", "-Command", "Start-Process -FilePath "+quotePowerShellString(path)).Run() case "darwin": - cmd := exec.Command("open", path) + cmd := exec.Command("open", codexAppDarwinOpenArgs(path)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() diff --git a/cmd/launch/codex_app_test.go b/cmd/launch/codex_app_test.go index 93e882c634..70636b9811 100644 --- a/cmd/launch/codex_app_test.go +++ b/cmd/launch/codex_app_test.go @@ -3055,6 +3055,81 @@ func TestCodexAppRestoreDoesNotTreatCLIProfileAsOwned(t *testing.T) { } } +func TestCodexAppDarwinOpenArgs(t *testing.T) { + for _, path := range []string{"", "/Applications/ChatGPT.app", "/Users/test/Apps/ChatGPT Preview.app"} { + t.Run(path, func(t *testing.T) { + setTestHome(t, t.TempDir()) + withCodexAppPlatform(t, "darwin") + t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434") + + usualArgs := []string{"-b", codexAppBundleID} + ollamaArgs := []string{"-b", codexAppBundleID, "codex://threads/new?mode=codex"} + if path != "" { + usualArgs = []string{path} + ollamaArgs = []string{"-a", path, "codex://threads/new?mode=codex"} + } + if got := codexAppDarwinOpenArgs(path); !slices.Equal(got, usualArgs) { + t.Fatalf("unconfigured open args = %q, want %q", got, usualArgs) + } + + app := &CodexApp{} + if err := app.ConfigureWithModels("qwen3:8b", testLaunchModels("qwen3:8b")); err != nil { + t.Fatal(err) + } + if got := codexAppDarwinOpenArgs(path); !slices.Equal(got, ollamaArgs) { + t.Fatalf("Ollama open args = %q, want %q", got, ollamaArgs) + } + + if err := restoreCodexAppProfile(); err != nil { + t.Fatal(err) + } + if got := codexAppDarwinOpenArgs(path); !slices.Equal(got, usualArgs) { + t.Fatalf("restored open args = %q, want %q", got, usualArgs) + } + }) + } +} + +func TestCodexAppDesktopLaunchMode(t *testing.T) { + setTestHome(t, t.TempDir()) + withCodexAppPlatform(t, "darwin") + withCodexAppRouterHealth(t, func() error { return nil }) + t.Setenv("OLLAMA_HOST", "http://127.0.0.1:11434") + + running := false + var openedArgs [][]string + withCodexAppProcessHooks(t, + func() bool { return running }, + func() error { running = false; return nil }, + func() error { + openedArgs = append(openedArgs, codexAppDarwinOpenArgs("/Applications/ChatGPT.app")) + running = true + return nil + }, + ) + codexAppCanOpenID = func() bool { return true } + + app := &CodexApp{} + if err := app.UseOllamaFromDesktop("qwen3:8b", testLaunchModels("qwen3:8b"), false); err != nil { + t.Fatal(err) + } + if err := app.RestartFromDesktop(true); err != nil { + t.Fatal(err) + } + if err := app.RestoreFromDesktop(true); err != nil { + t.Fatal(err) + } + + want := [][]string{ + {"-a", "/Applications/ChatGPT.app", "codex://threads/new?mode=codex"}, + {"-a", "/Applications/ChatGPT.app", "codex://threads/new?mode=codex"}, + {"/Applications/ChatGPT.app"}, + } + if !slices.EqualFunc(openedArgs, want, slices.Equal[[]string]) { + t.Fatalf("open args = %q, want first launch and restart in Codex, then a normal launch on restore: %q", openedArgs, want) + } +} + func TestCodexAppRunRestartsRunningAppWhenConfirmed(t *testing.T) { withCodexAppPlatform(t, "darwin") restoreConfirm := withLaunchConfirmPolicy(launchConfirmPolicy{yes: true})