diff --git a/AGENTS.md b/AGENTS.md index 3d010b14..b028fdc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,14 +97,14 @@ claude --plugin-dir ./apps/hook | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` or `true` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing entirely. Default: enabled. | | `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. | | `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. | -**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected. Prefer `PLANNOTATOR_REMOTE=1` for explicit control. +**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode. **Devcontainer/SSH usage:** ```bash diff --git a/apps/codex/README.md b/apps/codex/README.md index 2bab87ca..c9283a90 100644 --- a/apps/codex/README.md +++ b/apps/codex/README.md @@ -50,7 +50,7 @@ The message opens in the annotation UI where you can highlight text, add comment | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. | diff --git a/apps/copilot/README.md b/apps/copilot/README.md index 8af4ec79..0f4b944e 100644 --- a/apps/copilot/README.md +++ b/apps/copilot/README.md @@ -52,7 +52,7 @@ When you use plan mode in Copilot CLI: | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE` | Set to `disabled` to turn off URL sharing. | diff --git a/apps/hook/README.md b/apps/hook/README.md index d8c68a54..2f16ea0c 100644 --- a/apps/hook/README.md +++ b/apps/hook/README.md @@ -75,14 +75,14 @@ When Claude Code calls `ExitPlanMode`, this hook intercepts and: | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. | ## Remote / Devcontainer Usage -When running Claude Code in a remote environment (SSH, devcontainer, WSL), set these environment variables: +When running Claude Code in a remote environment (SSH, devcontainer, WSL), set `PLANNOTATOR_REMOTE=1` (or `true`) and these environment variables: ```bash export PLANNOTATOR_REMOTE=1 @@ -91,7 +91,7 @@ export PLANNOTATOR_PORT=9999 # Choose a port you'll forward This tells Plannotator to: - Use a fixed port instead of a random one (so you can set up port forwarding) -- Skip auto-opening the browser (since you'll open it manually on your local machine) +- Use remote-friendly port/browser handling for forwarded environments - Print the URL to the terminal for you to access **Port forwarding in VS Code devcontainers:** The port should be automatically forwarded. Check the "Ports" tab. diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index a17973c1..ad20c5fb 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -47,7 +47,7 @@ * --browser - Override which browser to open (e.g. "Google Chrome") * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (preferred) + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ diff --git a/apps/marketing/src/content/docs/getting-started/configuration.md b/apps/marketing/src/content/docs/getting-started/configuration.md index b1b32125..43dca687 100644 --- a/apps/marketing/src/content/docs/getting-started/configuration.md +++ b/apps/marketing/src/content/docs/getting-started/configuration.md @@ -12,7 +12,7 @@ Plannotator is configured through environment variables and hook/plugin configur | Variable | Default | Description | |----------|---------|-------------| -| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode. Uses a fixed port and skips browser auto-open. | +| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode, `0` or `false` to force local mode, or leave unset to auto-detect via `SSH_TTY` / `SSH_CONNECTION`. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | random (local) / `19432` (remote) | Fixed server port. Useful for port forwarding in remote environments. | | `PLANNOTATOR_BROWSER` | system default | Custom browser or script to open the UI. | | `PLANNOTATOR_SHARE` | enabled | Set to `disabled` to turn off URL sharing entirely. | @@ -65,7 +65,7 @@ Approved and denied plans are saved to `~/.plannotator/plans/` by default. You c ## Remote mode -When working over SSH, in a devcontainer, or in Docker, set `PLANNOTATOR_REMOTE=1` and `PLANNOTATOR_PORT` to a port you'll forward. See the [remote & devcontainers guide](/docs/guides/remote-and-devcontainers/) for setup instructions. +When working over SSH, in a devcontainer, or in Docker, set `PLANNOTATOR_REMOTE=1` (or `true`) and `PLANNOTATOR_PORT` to a port you'll forward. Set `PLANNOTATOR_REMOTE=0` / `false` if you need to force local behavior even when SSH env vars are present. See the [remote & devcontainers guide](/docs/guides/remote-and-devcontainers/) for setup instructions. ## Custom browser diff --git a/apps/marketing/src/content/docs/guides/remote-and-devcontainers.md b/apps/marketing/src/content/docs/guides/remote-and-devcontainers.md index 0a1b275e..829f2b01 100644 --- a/apps/marketing/src/content/docs/guides/remote-and-devcontainers.md +++ b/apps/marketing/src/content/docs/guides/remote-and-devcontainers.md @@ -6,11 +6,11 @@ sidebar: section: "Guides" --- -Plannotator works in remote environments — SSH sessions, VS Code Remote, devcontainers, and Docker. The key difference is that the browser can't auto-open on a headless server, so you need a fixed port and manual URL access. +Plannotator works in remote environments — SSH sessions, VS Code Remote, devcontainers, and Docker. The key difference is that remote sessions benefit from a fixed port for forwarding, and browser-opening behavior depends on your environment. ## Remote mode -Set `PLANNOTATOR_REMOTE=1` to enable remote mode: +Set `PLANNOTATOR_REMOTE=1` (or `true`) to force remote mode: ```bash export PLANNOTATOR_REMOTE=1 @@ -20,11 +20,11 @@ export PLANNOTATOR_PORT=9999 # Choose a port you'll forward Remote mode changes two behaviors: 1. **Fixed port** — Uses `PLANNOTATOR_PORT` (default: `19432`) instead of a random port, so you can set up port forwarding once -2. **No browser auto-open** — Prints the URL to the terminal instead of trying to open a browser +2. **Browser handling changes** — In headless setups you may need to open the forwarded URL manually instead of relying on browser auto-open ### Legacy detection -Plannotator also detects `SSH_TTY` and `SSH_CONNECTION` environment variables for automatic remote mode. However, `PLANNOTATOR_REMOTE=1` is preferred for explicit control. +Plannotator also detects `SSH_TTY` and `SSH_CONNECTION` environment variables for automatic remote mode when `PLANNOTATOR_REMOTE` is unset. Use `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `PLANNOTATOR_REMOTE=0` / `false` to force local mode. ## VS Code Remote / devcontainers @@ -62,7 +62,7 @@ Or forward ad-hoc when connecting: ssh -L 9999:localhost:9999 your-server ``` -Then open `http://localhost:9999` locally when Plannotator prints the URL. +Then open `http://localhost:9999` locally if Plannotator does not open a browser for you. ## Docker (without VS Code) diff --git a/apps/marketing/src/content/docs/reference/environment-variables.md b/apps/marketing/src/content/docs/reference/environment-variables.md index fccb5214..3b5a49f1 100644 --- a/apps/marketing/src/content/docs/reference/environment-variables.md +++ b/apps/marketing/src/content/docs/reference/environment-variables.md @@ -12,7 +12,7 @@ All Plannotator environment variables and their defaults. | Variable | Default | Description | |----------|---------|-------------| -| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode. Uses fixed port and skips browser auto-open. | +| `PLANNOTATOR_REMOTE` | auto-detect | Set to `1` or `true` to force remote mode, `0` or `false` to force local mode, or leave unset to auto-detect via `SSH_TTY` / `SSH_CONNECTION`. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | random (local) / `19432` (remote) | Fixed server port. When not set, local sessions use a random port; remote sessions default to `19432`. | | `PLANNOTATOR_BROWSER` | system default | Custom browser to open the UI in. macOS: app name or path. Linux/Windows: executable path. Can also be a script. Takes priority over `BROWSER`. Also settable per-invocation with `--browser`. | | `BROWSER` | (none) | Standard env var for specifying a browser. VS Code sets this automatically in devcontainers. Used as fallback when `PLANNOTATOR_BROWSER` is not set. | @@ -46,11 +46,11 @@ When running your own paste service binary, these variables configure it: ## Remote mode behavior -When `PLANNOTATOR_REMOTE=1` or SSH is detected: +When remote mode is forced with `PLANNOTATOR_REMOTE=1` / `true`, or SSH is detected while `PLANNOTATOR_REMOTE` is unset: - Server binds to `PLANNOTATOR_PORT` (default `19432`) instead of a random port -- Browser auto-open is skipped -- The URL is printed to stderr for manual access +- Browser-opening behavior depends on the environment and configured browser handler +- In headless setups, you may need to open the forwarded URL manually ### Legacy SSH detection @@ -61,7 +61,7 @@ These environment variables are still detected for backwards compatibility: | `SSH_TTY` | Set by SSH when a TTY is allocated | | `SSH_CONNECTION` | Set by SSH with connection details | -If either is present, Plannotator enables remote mode automatically. Prefer `PLANNOTATOR_REMOTE=1` for explicit control. +If either is present, Plannotator enables remote mode automatically when `PLANNOTATOR_REMOTE` is unset. Set `PLANNOTATOR_REMOTE=1` / `true` to force remote mode or `0` / `false` to force local mode. ## Port resolution order diff --git a/apps/opencode-plugin/README.md b/apps/opencode-plugin/README.md index 8918601f..6053ef1a 100644 --- a/apps/opencode-plugin/README.md +++ b/apps/opencode-plugin/README.md @@ -56,7 +56,7 @@ Restart OpenCode. The `submit_plan` tool is now available. | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection. Uses a fixed port in remote mode; browser-opening behavior depends on the environment. | | `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. | | `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. | | `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. | @@ -76,7 +76,7 @@ Works in containerized environments. Set the env vars and forward the port: } ``` -Then open `http://localhost:9999` when `submit_plan` is called. +If nothing opens automatically, open `http://localhost:9999` when `submit_plan` is called. See [devcontainer.md](./devcontainer.md) for full setup details. diff --git a/apps/opencode-plugin/devcontainer.md b/apps/opencode-plugin/devcontainer.md index 0cbcdbbe..62e43533 100644 --- a/apps/opencode-plugin/devcontainer.md +++ b/apps/opencode-plugin/devcontainer.md @@ -18,7 +18,7 @@ Add these to your `devcontainer.json`: | Variable | Purpose | |----------|---------| -| `PLANNOTATOR_REMOTE=1` | Tells Plannotator not to open a browser (required in containers) | +| `PLANNOTATOR_REMOTE=1` | Forces remote mode for container-friendly port/browser handling (required in containers) | | `PLANNOTATOR_PORT=9999` | Fixed port for the UI (required for port forwarding) | Both are required. Just setting the port isn't enough. @@ -35,7 +35,7 @@ Ensure port 9999 (or your chosen port) is forwarded to your host. In VS Code dev 4. Open `http://localhost:9999` in your host browser 5. Approve or deny the plan -**Note:** There's no browser auto-open or notification in remote mode. You'll need to manually navigate to the URL when you see the agent call `submit_plan`. +**Note:** Browser opening depends on your container/browser setup. If nothing opens automatically, navigate to the forwarded URL manually when you see the agent call `submit_plan`. ## OpenCode Web @@ -49,7 +49,7 @@ Ensure port 9999 (or your chosen port) is forwarded to your host. In VS Code dev ## Legacy Support -If your environment already has `SSH_TTY` or `SSH_CONNECTION` set (common in SSH sessions), Plannotator will detect remote mode automatically. The `PLANNOTATOR_REMOTE` env var is preferred for Docker/devcontainer setups where those aren't present. +If your environment already has `SSH_TTY` or `SSH_CONNECTION` set (common in SSH sessions), Plannotator will detect remote mode automatically when `PLANNOTATOR_REMOTE` is unset. You can also force local mode with `PLANNOTATOR_REMOTE=false` or `0`. ## Troubleshooting diff --git a/apps/opencode-plugin/index.ts b/apps/opencode-plugin/index.ts index dec798fd..8c4d42ac 100644 --- a/apps/opencode-plugin/index.ts +++ b/apps/opencode-plugin/index.ts @@ -9,7 +9,7 @@ * revisions and resubmit with the file path. * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (devcontainer, SSH) + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) * PLANNOTATOR_PLAN_TIMEOUT_SECONDS - Max wait for approval (default: 345600, set 0 to disable) * PLANNOTATOR_ALLOW_SUBAGENTS - Set to "1" to allow subagents to see submit_plan diff --git a/apps/pi-extension/server/network.test.ts b/apps/pi-extension/server/network.test.ts new file mode 100644 index 00000000..c261b32b --- /dev/null +++ b/apps/pi-extension/server/network.test.ts @@ -0,0 +1,96 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { getServerPort, isRemoteSession } from "./network"; + +const savedEnv: Record = {}; +const envKeys = ["PLANNOTATOR_REMOTE", "PLANNOTATOR_PORT", "SSH_TTY", "SSH_CONNECTION"]; + +function clearEnv() { + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } +} + +afterEach(() => { + for (const key of envKeys) { + if (savedEnv[key] !== undefined) { + process.env[key] = savedEnv[key]; + } else { + delete process.env[key]; + } + } +}); + +describe("pi remote detection", () => { + test("false by default", () => { + clearEnv(); + expect(isRemoteSession()).toBe(false); + }); + + test("true when PLANNOTATOR_REMOTE=1", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "1"; + expect(isRemoteSession()).toBe(true); + }); + + test("true when PLANNOTATOR_REMOTE=true", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "true"; + expect(isRemoteSession()).toBe(true); + }); + + test("false when PLANNOTATOR_REMOTE=0", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "0"; + expect(isRemoteSession()).toBe(false); + }); + + test("false when PLANNOTATOR_REMOTE=false", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + expect(isRemoteSession()).toBe(false); + }); + + test("PLANNOTATOR_REMOTE=false overrides SSH_TTY", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + process.env.SSH_TTY = "/dev/pts/0"; + expect(isRemoteSession()).toBe(false); + }); + + test("PLANNOTATOR_REMOTE=0 overrides SSH_CONNECTION", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "0"; + process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22"; + expect(isRemoteSession()).toBe(false); + }); + + test("true when SSH_TTY is set and env var is unset", () => { + clearEnv(); + process.env.SSH_TTY = "/dev/pts/0"; + expect(isRemoteSession()).toBe(true); + }); +}); + +describe("pi port selection", () => { + test("uses random local port when false overrides SSH", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + process.env.SSH_TTY = "/dev/pts/0"; + expect(getServerPort()).toEqual({ port: 0, portSource: "random" }); + }); + + test("uses default remote port when SSH is detected", () => { + clearEnv(); + process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22"; + expect(getServerPort()).toEqual({ port: 19432, portSource: "remote-default" }); + }); + + test("PLANNOTATOR_PORT still takes precedence", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + process.env.SSH_TTY = "/dev/pts/0"; + process.env.PLANNOTATOR_PORT = "9999"; + expect(getServerPort()).toEqual({ port: 9999, portSource: "env" }); + }); +}); diff --git a/apps/pi-extension/server/network.ts b/apps/pi-extension/server/network.ts index 07d81dd0..1399aa05 100644 --- a/apps/pi-extension/server/network.ts +++ b/apps/pi-extension/server/network.ts @@ -11,13 +11,30 @@ const DEFAULT_REMOTE_PORT = 19432; /** * Check if running in a remote session (SSH, devcontainer, etc.) - * Honors PLANNOTATOR_REMOTE env var, or detects SSH_TTY/SSH_CONNECTION. + * Honors PLANNOTATOR_REMOTE as a tri-state override, or detects SSH_TTY/SSH_CONNECTION. */ -function isRemoteSession(): boolean { +function getRemoteOverride(): boolean | null { const remote = process.env.PLANNOTATOR_REMOTE; + if (remote === undefined) { + return null; + } + if (remote === "1" || remote?.toLowerCase() === "true") { return true; } + + if (remote === "0" || remote?.toLowerCase() === "false") { + return false; + } + + return null; +} + +export function isRemoteSession(): boolean { + const remoteOverride = getRemoteOverride(); + if (remoteOverride !== null) { + return remoteOverride; + } // Legacy SSH detection if (process.env.SSH_TTY || process.env.SSH_CONNECTION) { return true; @@ -32,7 +49,7 @@ function isRemoteSession(): boolean { * - Local sessions use random port * Returns { port, portSource } so caller can notify user if needed. */ -function getServerPort(): { +export function getServerPort(): { port: number; portSource: "env" | "remote-default" | "random"; } { @@ -98,7 +115,7 @@ export async function listenOnPort( /** * Open URL in system browser (Node-compatible, no Bun $ dependency). - * Honors PLANNOTATOR_BROWSER and BROWSER env vars, matching packages/server/browser.ts. + * Honors PLANNOTATOR_BROWSER and BROWSER env vars. * Returns { opened: true } if browser was opened, { opened: false, isRemote: true, url } if remote session. */ export function openBrowser(url: string): { diff --git a/apps/portal/ANNOTATE.md b/apps/portal/ANNOTATE.md index 3c346aa9..3594d202 100644 --- a/apps/portal/ANNOTATE.md +++ b/apps/portal/ANNOTATE.md @@ -175,6 +175,6 @@ The annotate server respects the same environment variables as plan review: | Variable | Description | |----------|-------------| -| `PLANNOTATOR_REMOTE` | Set to `1` for remote/SSH mode (fixed port, no browser open) | +| `PLANNOTATOR_REMOTE` | Set to `1` / `true` for remote mode, `0` / `false` for local mode, or leave unset for SSH auto-detection (fixed port in remote mode; browser behavior depends on the environment) | | `PLANNOTATOR_PORT` | Fixed port (default: random locally, `19432` for remote) | | `PLANNOTATOR_BROWSER` | Custom browser to open the UI in | diff --git a/apps/review/server/index.ts b/apps/review/server/index.ts index dfefa7c5..8ed15b65 100644 --- a/apps/review/server/index.ts +++ b/apps/review/server/index.ts @@ -11,7 +11,7 @@ * bun apps/review/server/index.ts HEAD~5..HEAD # Commit range * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ @@ -70,7 +70,7 @@ const server = await startReviewServer({ handleReviewServerReady(url, isRemote, port); console.error(`Code review at ${url}`); if (isRemote) { - console.error(`(Remote mode - manually open the URL above)`); + console.error(`(Remote mode detected — if no browser opens automatically, use the URL above)`); } }, }); diff --git a/packages/server/annotate.ts b/packages/server/annotate.ts index 078ce789..373880c2 100644 --- a/packages/server/annotate.ts +++ b/packages/server/annotate.ts @@ -7,7 +7,7 @@ * render it without modifications. * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote/devcontainer mode + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ diff --git a/packages/server/browser.test.ts b/packages/server/browser.test.ts new file mode 100644 index 00000000..be2a5ff6 --- /dev/null +++ b/packages/server/browser.test.ts @@ -0,0 +1,46 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { shouldTryRemoteBrowserFallback } from "./browser"; + +const savedEnv: Record = {}; +const envKeys = ["PLANNOTATOR_BROWSER", "BROWSER"]; + +function clearEnv() { + for (const key of envKeys) { + savedEnv[key] = process.env[key]; + delete process.env[key]; + } +} + +afterEach(() => { + for (const key of envKeys) { + if (savedEnv[key] !== undefined) { + process.env[key] = savedEnv[key]; + } else { + delete process.env[key]; + } + } +}); + +describe("shouldTryRemoteBrowserFallback", () => { + test("false for local sessions", () => { + clearEnv(); + expect(shouldTryRemoteBrowserFallback(false)).toBe(false); + }); + + test("true for remote sessions without browser handlers", () => { + clearEnv(); + expect(shouldTryRemoteBrowserFallback(true)).toBe(true); + }); + + test("false for remote sessions with BROWSER configured", () => { + clearEnv(); + process.env.BROWSER = "/usr/bin/browser"; + expect(shouldTryRemoteBrowserFallback(true)).toBe(false); + }); + + test("false for remote sessions with PLANNOTATOR_BROWSER configured", () => { + clearEnv(); + process.env.PLANNOTATOR_BROWSER = "/usr/bin/browser"; + expect(shouldTryRemoteBrowserFallback(true)).toBe(false); + }); +}); diff --git a/packages/server/browser.ts b/packages/server/browser.ts index 65b8685c..68a85375 100644 --- a/packages/server/browser.ts +++ b/packages/server/browser.ts @@ -75,9 +75,23 @@ export async function isWSL(): Promise { * * Fails silently if browser can't be opened */ -export async function openBrowser(url: string): Promise { +export function shouldTryRemoteBrowserFallback(isRemote: boolean): boolean { + return isRemote && !process.env.PLANNOTATOR_BROWSER && !process.env.BROWSER; +} + +export async function openBrowser( + url: string, + options?: { isRemote?: boolean } +): Promise { try { const browser = process.env.PLANNOTATOR_BROWSER || process.env.BROWSER; + if (shouldTryRemoteBrowserFallback(options?.isRemote ?? false)) { + const openedViaIpc = await tryVscodeIpc(url); + if (openedViaIpc) { + return true; + } + } + const platform = process.platform; const wsl = await isWSL(); diff --git a/packages/server/index.ts b/packages/server/index.ts index 38e7d5a6..fe8e1bfc 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -4,7 +4,7 @@ * Provides a consistent server implementation for both Claude Code and OpenCode plugins. * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote/devcontainer mode + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) * PLANNOTATOR_ORIGIN - Origin identifier ("claude-code" or "opencode") */ diff --git a/packages/server/remote.test.ts b/packages/server/remote.test.ts index 4d4922b4..6cc4eba9 100644 --- a/packages/server/remote.test.ts +++ b/packages/server/remote.test.ts @@ -46,6 +46,32 @@ describe("isRemoteSession", () => { expect(isRemoteSession()).toBe(true); }); + test("false when PLANNOTATOR_REMOTE=0", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "0"; + expect(isRemoteSession()).toBe(false); + }); + + test("false when PLANNOTATOR_REMOTE=false", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + expect(isRemoteSession()).toBe(false); + }); + + test("PLANNOTATOR_REMOTE=false overrides SSH_TTY", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + process.env.SSH_TTY = "/dev/pts/0"; + expect(isRemoteSession()).toBe(false); + }); + + test("PLANNOTATOR_REMOTE=0 overrides SSH_CONNECTION", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "0"; + process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22"; + expect(isRemoteSession()).toBe(false); + }); + test("true when SSH_TTY is set (legacy)", () => { clearEnv(); process.env.SSH_TTY = "/dev/pts/0"; @@ -71,6 +97,13 @@ describe("getServerPort", () => { expect(getServerPort()).toBe(19432); }); + test("returns 0 when PLANNOTATOR_REMOTE=false overrides SSH", () => { + clearEnv(); + process.env.PLANNOTATOR_REMOTE = "false"; + process.env.SSH_TTY = "/dev/pts/0"; + expect(getServerPort()).toBe(0); + }); + test("explicit PLANNOTATOR_PORT overrides everything", () => { clearEnv(); process.env.PLANNOTATOR_PORT = "8080"; diff --git a/packages/server/remote.ts b/packages/server/remote.ts index 75c2f04a..379afca0 100644 --- a/packages/server/remote.ts +++ b/packages/server/remote.ts @@ -2,7 +2,7 @@ * Remote session detection and port configuration * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" to force remote mode (preferred) + * PLANNOTATOR_REMOTE - Set to "1"/"true" to force remote, "0"/"false" to force local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) * * Legacy (still supported): SSH_TTY, SSH_CONNECTION @@ -10,14 +10,30 @@ const DEFAULT_REMOTE_PORT = 19432; +function getRemoteOverride(): boolean | null { + const remote = process.env.PLANNOTATOR_REMOTE; + if (remote === undefined) { + return null; + } + + if (remote === "1" || remote?.toLowerCase() === "true") { + return true; + } + + if (remote === "0" || remote?.toLowerCase() === "false") { + return false; + } + + return null; +} + /** * Check if running in a remote session (SSH, devcontainer, etc.) */ export function isRemoteSession(): boolean { - // New preferred env var - const remote = process.env.PLANNOTATOR_REMOTE; - if (remote === "1" || remote?.toLowerCase() === "true") { - return true; + const remoteOverride = getRemoteOverride(); + if (remoteOverride !== null) { + return remoteOverride; } // Legacy: SSH_TTY/SSH_CONNECTION (deprecated, silent) diff --git a/packages/server/review.ts b/packages/server/review.ts index 37904810..de88298b 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -5,7 +5,7 @@ * Follows the same patterns as the plan server. * * Environment variables: - * PLANNOTATOR_REMOTE - Set to "1" or "true" for remote/devcontainer mode + * PLANNOTATOR_REMOTE - Set to "1"/"true" for remote, "0"/"false" for local * PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote) */ diff --git a/packages/server/shared-handlers.ts b/packages/server/shared-handlers.ts index 5b702c77..83675d3d 100644 --- a/packages/server/shared-handlers.ts +++ b/packages/server/shared-handlers.ts @@ -135,11 +135,11 @@ export function handleFavicon(): Response { }); } -/** Open browser for local sessions or when a custom handler (e.g. VS Code extension) is configured. */ +/** Attempt to open the browser for the session URL. */ export async function handleServerReady( url: string, isRemote: boolean, _port: number, ): Promise { - await openBrowser(url); + await openBrowser(url, { isRemote }); }