aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.me>2026-08-30 16:27:22 +0100
committerIgor Soarez <igor@soarez.org>2026-10-14 12:19:26 +0100
commitc8fd7b7078da841d5537aee9334291887445889f (patch)
tree2ee3611d169dc872bde0af8be186e1356af17e7d /src
parent3a0bfc3af446a947f8392aaa3e3b2605ea7ad2da (diff)
fix(profile): never prompt for a profile when driving a remote browser
Profile selection works by launching the browser with --profile-directory and letting ProcessSingleton delegate to the running process, which only reaches a browser on this machine. discoverEndpoint() already withholds userDataDir for a remote target, but resolveUserDataDir() fell through to the local candidate dirs, so the picker offered this disk's Chromium profiles for a browser on another host - on castle, Brave's profiles for a Chrome on :9223. Pinning one launched a local browser and polled the remote for a sentinel that could never appear, failing after the seed deadline; seedProfileWindow()'s mismatch guard missed it because connectedDir is undefined on the remote path. Setup now returns early instead of prompting - the question had no actionable answer, and since cancelling stores nothing it returned every session. /browser-profile and seedProfileWindow() refuse with the reason, the latter catching a pin left over from a local session.
Diffstat (limited to 'src')
-rw-r--r--src/cdp/current-target.ts27
-rw-r--r--src/profile/bind.ts16
-rw-r--r--src/profile/command.ts24
3 files changed, 67 insertions, 0 deletions
diff --git a/src/cdp/current-target.ts b/src/cdp/current-target.ts
new file mode 100644
index 0000000..82bebe6
--- /dev/null
+++ b/src/cdp/current-target.ts
@@ -0,0 +1,27 @@
+/**
+ * The target the harness is actually pointed at, resolved with I/O.
+ *
+ * `resolveCdpTarget` in endpoint.ts is deliberately pure, so every caller that
+ * needs the *effective* target has to read the stored choice first. This wraps
+ * that pair so callers outside the CDP layer — profile selection in
+ * particular — can ask "am I driving a browser on this machine?" without
+ * duplicating the precedence rules.
+ */
+
+import { type CdpTarget, resolveCdpTarget } from "./endpoint";
+import { readTarget } from "./target-store";
+
+/** The effective target: BU_CDP_HTTP, else /browser-target, else the default. */
+export const currentCdpTarget = async (): Promise<CdpTarget> =>
+ resolveCdpTarget(process.env, await readTarget());
+
+/**
+ * True when the harness is driving a browser on another host.
+ *
+ * Profile selection is meaningless in that case: it works by launching the
+ * browser binary with --profile-directory and letting ProcessSingleton hand
+ * the argv to the running browser (see profile/launch.ts), which only reaches
+ * a browser on this machine.
+ */
+export const drivingRemoteBrowser = async (): Promise<boolean> =>
+ (await currentCdpTarget()).kind === "remote";
diff --git a/src/profile/bind.ts b/src/profile/bind.ts
index 95cffaa..5382547 100644
--- a/src/profile/bind.ts
+++ b/src/profile/bind.ts
@@ -1,6 +1,8 @@
import type { BrowserClient } from "../client";
import type { ResultOf } from "../cdp/commands";
import { type CdpError, cdpError } from "../cdp/errors";
+import { currentCdpTarget } from "../cdp/current-target";
+import { describeCdpTarget } from "../cdp/endpoint";
import { type Result, err, ok } from "../util/result";
import { detectRunningBrowser, resolveBrowserExecutable } from "./browser-process";
import { openProfileWindow } from "./launch";
@@ -25,6 +27,20 @@ export const seedProfileWindow = async (
client: BrowserClient,
pin: ProfilePin,
): Promise<Result<string, CdpError>> => {
+ // A pin left over from a local session is not actionable against a browser on
+ // another host: the launch below would start a browser HERE and then poll the
+ // REMOTE one for a sentinel that cannot appear, failing only after the seed
+ // deadline. Refuse up front and say why.
+ const target = await currentCdpTarget();
+ if (target.kind === "remote") {
+ return err(cdpError(
+ "discovery_failed",
+ `the selected profile "${pin.label}" belongs to a browser on this machine, but the harness is driving ` +
+ `${describeCdpTarget(target)}. Run /browser-profile and clear the selection, or /browser-target local to ` +
+ "drive this machine's browser.",
+ ));
+ }
+
const connectedDir = client.userDataDir();
if (connectedDir && connectedDir !== pin.userDataDir) {
return err(cdpError(
diff --git a/src/profile/command.ts b/src/profile/command.ts
index 270b42f..ea0ddcc 100644
--- a/src/profile/command.ts
+++ b/src/profile/command.ts
@@ -1,6 +1,8 @@
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import type { BrowserClient } from "../client";
import { discoverEndpoint } from "../cdp/discovery";
+import { currentCdpTarget } from "../cdp/current-target";
+import { describeCdpTarget } from "../cdp/endpoint";
import { type Result, err, ok } from "../util/result";
import { type BrowserProfile, listProfiles } from "./list";
import { browserNameForUserDataDir, userDataDirCandidates } from "./paths";
@@ -60,6 +62,21 @@ const resolveUserDataDir = async (client: BrowserClient): Promise<Result<string,
const endpoint = await discoverEndpoint();
if (endpoint.success && endpoint.data.userDataDir) return ok(endpoint.data.userDataDir);
+ // A remote browser has no user-data-dir on THIS disk. Falling through to the
+ // local candidates below would enumerate whatever Chromium browsers happen to
+ // be installed here and offer their profiles for a browser on another host —
+ // profiles the harness is not connected to and cannot open a window in. Worse,
+ // pinning one sends seedProfileWindow() off to launch a local browser whose
+ // sentinel page can never appear over this connection.
+ const target = await currentCdpTarget();
+ if (target.kind === "remote") {
+ return err(
+ `Profile selection needs a browser on this machine, but the harness is driving ${describeCdpTarget(target)}. ` +
+ "Choosing a profile works by launching the browser with --profile-directory, which only reaches a local browser. " +
+ "Run /browser-target local to drive this machine's browser, then /browser-profile.",
+ );
+ }
+
const withProfiles: string[] = [];
for (const dir of userDataDirCandidates()) {
if ((await listProfiles(dir)).length > 0) withProfiles.push(dir);
@@ -164,6 +181,13 @@ export const promptForProfileIfUnset = async (
client.setProfilePin(stored);
return undefined;
}
+ // Never prompt for a remote browser. Profile selection cannot act on one (see
+ // resolveUserDataDir), so asking would be a blocking question with no useful
+ // answer — and every cancelled prompt stores nothing, so it would come back
+ // every session. Silence is correct here: the browser keeps its own default
+ // profile, and /browser-status already reports which browser is being driven.
+ if ((await currentCdpTarget()).kind === "remote") return undefined;
+
if (!ctx?.hasUI) {
return "No browser profile selected — using whichever window is focused. Run /browser-profile to pin one.";
}