Skip to content

Browser Fallback Not Working on Linux in Newer Pi Versions #6

@borysow

Description

@borysow

First off, I love a lot of your extensions, and in particular this one that I take with me everywhere. So, thank you for that :) If the below isn't helpful (sorry, I'm a Python guy, no javascript experience), then I'm deeply sorry.

Anyway, I'm on Linux and thus can't use the glimpseui, at some point pi implemented some changes that broke the browser fallback. I'll try to keep this concise, and I deeply hope this is helpful.

pi-interview version affected: 0.6.0 (and older)
Observed Behavior:

  • Interview attempts to launch but browser doesn't even open.

(I have not tested each version)
Last known compatible pi version: 0.58.0
Known broken pi version: 0.64.0

Agent suspects break happened in 0.61.0 (which is close to when I remember it breaking).

Agent's assessment:

## Likely weak points
1. Linux open path currently relies on `pi.exec("xdg-open", ...)`.
2. Queue detection can suppress auto-open entirely when another session is detected.
3. Browser-open failure currently tends to abort flow instead of always providing manual URL fallback.

## Local workaround that fixed it
I patched `index.ts` to:
- Use detached Linux launchers first (`xdg-open`, `sensible-browser`, `gio open`, configured browser).
- Keep `pi.exec` as last fallback.
- Continue auto-open even when queue is detected (still show queue info).
- If auto-open fails, keep interview server alive and show manual URL instead of failing hard.

This is the fix that the agent implemented that did in fact fix things,

--- a/index.ts
+++ b/index.ts
@@
-import { execSync, execFileSync } from "node:child_process";
+import { execSync, execFileSync, spawn } from "node:child_process";
@@
+function spawnDetached(command: string, args: string[]): void {
+	const child = spawn(command, args, {
+		detached: true,
+		stdio: "ignore",
+		shell: false,
+	});
+	child.unref();
+}
+
 async function openUrl(pi: ExtensionAPI, url: string, browser?: string): Promise<void> {
 	const platform = os.platform();
+
+	if (platform === "linux") {
+		const launchErrors: string[] = [];
+		const launchers: Array<[string, string[]]> = browser
+			? [[browser, [url]]]
+			: [["xdg-open", [url]], ["sensible-browser", [url]], ["gio", ["open", url]]];
+
+		for (const [command, args] of launchers) {
+			try {
+				spawnDetached(command, args);
+				return;
+			} catch (err) {
+				const message = err instanceof Error ? err.message : String(err);
+				launchErrors.push(`${command}: ${message}`);
+			}
+		}
+
+		const fallback = browser ? await pi.exec(browser, [url]) : await pi.exec("xdg-open", [url]);
+		if (fallback.code === 0) return;
+		throw new Error(
+			`Failed to open browser. Tried detached launchers (${launchErrors.join("; ")}) ` +
+			`and fallback exec (exit ${fallback.code}): ${fallback.stderr || fallback.stdout || "no output"}`
+		);
+	}
@@
-						if (otherActive.length > 0) {
+						if (otherActive.length > 0) {
 							// keep queued messaging, but DO NOT block auto-open
 							...
-						} else {
-							// open browser only in else branch (old behavior)
-						}
+						}
+
+						// Always attempt auto-open, even if another session exists
+						...
@@
-							try {
-								await openUrl(pi, url, settings.browser);
-							} catch (err) {
-								cleanup();
-								const message = err instanceof Error ? err.message : String(err);
-								reject(new Error(`Failed to open browser: ${message}`));
-							}
+							try {
+								await openUrl(pi, url, settings.browser);
+							} catch (err) {
+								// Keep interview alive, emit manual URL fallback instead of failing hard
+								const message = err instanceof Error ? err.message : String(err);
+								const fallbackText = `Could not auto-open browser: ${message}\nOpen manually: ${url}`;
+								if (onUpdate) {
+									onUpdate({
+										content: [{ type: "text", text: fallbackText }],
+										details: { status: "queued", url, responses: [], queuedMessage: fallbackText },
+									});
+								} else if (pi.hasUI) {
+									pi.ui.notify(`Open interview manually: ${url}`, "warning");
+								}
+							}

Again, sorry if this isn't super helpful. Trying to give back as best as I'm able.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions