diff --git a/packages/server/shared-handlers.test.ts b/packages/server/shared-handlers.test.ts index d12fd03e..2856c46e 100644 --- a/packages/server/shared-handlers.test.ts +++ b/packages/server/shared-handlers.test.ts @@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test"; import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { handleSaveNotes, handleServerReady, writeServerReadyMetadata } from "./shared-handlers"; +import { + handleSaveNotes, + handleServerReady, + isCodexDesktopHost, + writeServerReadyMetadata, +} from "./shared-handlers"; function saveNotesRequest(body: unknown): Request { return new Request("http://localhost/api/save-notes", { @@ -104,15 +109,30 @@ describe("writeServerReadyMetadata", () => { }); describe("handleServerReady", () => { + test("detects the Codex Desktop app host", () => { + expect(isCodexDesktopHost({ __CFBundleIdentifier: "com.openai.codex" })).toBe(true); + expect(isCodexDesktopHost({ __CFBundleIdentifier: "com.apple.Terminal" })).toBe(false); + }); + test("does not open a browser when host-plugin mode handles it", async () => { let opened = false; + const originalBundleIdentifier = process.env.__CFBundleIdentifier; + process.env.__CFBundleIdentifier = "com.apple.Terminal"; - await handleServerReady("http://localhost:12345", false, 12345, { - skipBrowserOpen: true, - openBrowser: async () => { - opened = true; - }, - }); + try { + await handleServerReady("http://localhost:12345", false, 12345, { + skipBrowserOpen: true, + openBrowser: async () => { + opened = true; + }, + }); + } finally { + if (originalBundleIdentifier === undefined) { + delete process.env.__CFBundleIdentifier; + } else { + process.env.__CFBundleIdentifier = originalBundleIdentifier; + } + } expect(opened).toBe(false); }); @@ -141,10 +161,12 @@ describe("handleServerReady", () => { const writes: string[] = []; let opened = ""; const original = process.stderr.write.bind(process.stderr); + const originalBundleIdentifier = process.env.__CFBundleIdentifier; (process.stderr as { write: unknown }).write = (chunk: unknown) => { writes.push(String(chunk)); return true; }; + process.env.__CFBundleIdentifier = "com.apple.Terminal"; try { await handleServerReady("http://localhost:3000", false, 3000, { openBrowser: async (u: string) => { @@ -154,11 +176,40 @@ describe("handleServerReady", () => { }); } finally { (process.stderr as { write: unknown }).write = original; + if (originalBundleIdentifier === undefined) { + delete process.env.__CFBundleIdentifier; + } else { + process.env.__CFBundleIdentifier = originalBundleIdentifier; + } } expect(writes.join("")).not.toContain("http://localhost:3000"); expect(opened).toBe("http://localhost:3000"); }); + test("prints the URL for a local Codex Desktop session even when the browser opens", async () => { + const writes: string[] = []; + const originalWrite = process.stderr.write.bind(process.stderr); + const originalBundleIdentifier = process.env.__CFBundleIdentifier; + (process.stderr as { write: unknown }).write = (chunk: unknown) => { + writes.push(String(chunk)); + return true; + }; + process.env.__CFBundleIdentifier = "com.openai.codex"; + try { + await handleServerReady("http://localhost:3000", false, 3000, { + openBrowser: async () => true, + }); + } finally { + (process.stderr as { write: unknown }).write = originalWrite; + if (originalBundleIdentifier === undefined) { + delete process.env.__CFBundleIdentifier; + } else { + process.env.__CFBundleIdentifier = originalBundleIdentifier; + } + } + expect(writes.join("")).toContain("http://localhost:3000"); + }); + // Regression: a local session whose browser can't be opened (headless box, // devcontainer with no display) must still surface the URL, or the agent // hangs at waitForDecision with the user having no link to visit. diff --git a/packages/server/shared-handlers.ts b/packages/server/shared-handlers.ts index dec45cce..e6e7c333 100644 --- a/packages/server/shared-handlers.ts +++ b/packages/server/shared-handlers.ts @@ -177,6 +177,10 @@ export function writeServerReadyMetadata(readyFile: string, metadata: ServerRead appendFileSync(readyFile, `${JSON.stringify(metadata)}\n`, "utf8"); } +export function isCodexDesktopHost(env: NodeJS.ProcessEnv = process.env): boolean { + return env.__CFBundleIdentifier === "com.openai.codex"; +} + /** Attempt to open the browser for the session URL. */ export async function handleServerReady( url: string, @@ -203,6 +207,8 @@ export async function handleServerReady( process.stderr.write( `\n Plannotator session ready — open on your local machine (forward port ${port} if needed):\n ${url}\n\n`, ); + } else if (isCodexDesktopHost()) { + process.stderr.write(`\n Plannotator session ready:\n ${url}\n\n`); } const skipBrowserOpen = options.skipBrowserOpen ?? process.env.PLANNOTATOR_SKIP_BROWSER_OPEN === "1";