fix: use PowerShell Start-Process for Windows browser launch, prevent URL wrapping (#1870)

Closes #1574
This commit is contained in:
TÂCHES 2026-03-21 15:12:24 -06:00 committed by GitHub
parent 0188b8eaa8
commit 77b220e9e5
4 changed files with 29 additions and 30 deletions

View file

@ -1,7 +1,7 @@
// GSD Login Dialog Component — OAuth login flow UI
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { getOAuthProviders } from "@gsd/pi-ai/oauth";
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@gsd/pi-tui";
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, truncateToWidth, type TUI } from "@gsd/pi-tui";
import { execFile } from "child_process";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
@ -121,21 +121,25 @@ export class LoginDialogComponent extends Container implements Focusable {
showAuth(url: string, instructions?: string): void {
this.contentContainer.clear();
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
// Truncate the visible URL text so it never wraps (which would break
// the OSC 8 hyperlink). The full URL is still the link target.
const maxUrlWidth = Math.max(20, this.tui.terminal.columns - 4);
const displayUrl = truncateToWidth(url, maxUrlWidth);
const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`;
this.contentContainer.addChild(new Text(urlLink, 1, 0));
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0));
if (instructions) {
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
}
// Try to open browser — on Windows, `start` needs an empty title arg
// so it treats the URL as a target, not a window title
// PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not.
if (process.platform === "win32") {
execFile("cmd", ["/c", "start", "", url], () => {});
execFile("powershell", ["-c", `Start-Process '${url.replace(/'/g, "''")}'`], () => {});
} else {
const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
execFile(openCmd, [url], () => {});

View file

@ -124,7 +124,8 @@ async function loadPico(): Promise<PicoModule> {
/** Open a URL in the system browser (best-effort, non-blocking) */
function openBrowser(url: string): void {
if (process.platform === 'win32') {
execFile('cmd', ['/c', 'start', '', url], () => {})
// PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not.
execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], () => {})
} else {
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
execFile(cmd, [url], () => {})

View file

@ -4,7 +4,7 @@
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
import { writeFileSync, mkdirSync } from "node:fs";
import { join, basename } from "node:path";
import { exec } from "node:child_process";
import { exec, execFile } from "node:child_process";
import {
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
@ -20,20 +20,13 @@ import { getErrorMessage } from "./error-utils.js";
* Non-blocking, non-fatal failures are silently ignored.
*/
export function openInBrowser(filePath: string): void {
const cmd =
process.platform === "darwin" ? "open" :
process.platform === "win32" ? "start" :
"xdg-open";
// On Windows, `start` needs an empty title argument when the path has spaces
const args = process.platform === "win32"
? `"" "${filePath}"`
: `"${filePath}"`;
exec(`${cmd} ${args}`, (err) => {
// Non-fatal — if the browser can't be opened, the file path is still shown
if (err) void err;
});
if (process.platform === "win32") {
// PowerShell's Start-Process handles paths with '&' and spaces safely.
execFile("powershell", ["-c", `Start-Process '${filePath.replace(/'/g, "''")}'`], () => {});
} else {
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
execFile(cmd, [filePath], () => {});
}
}
/**

View file

@ -1,5 +1,5 @@
import { randomBytes } from 'node:crypto'
import { exec, spawn, type ChildProcess, type SpawnOptions } from 'node:child_process'
import { exec, execFile, spawn, type ChildProcess, type SpawnOptions } from 'node:child_process'
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
import { request as httpRequest } from 'node:http'
import { createServer } from 'node:net'
@ -12,12 +12,13 @@ const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '.
/** Open a URL in the user's default browser. */
function openBrowser(url: string): void {
const cmd = process.platform === 'darwin' ? 'open' :
process.platform === 'win32' ? 'start' :
'xdg-open'
exec(`${cmd} "${url}"`, () => {
// Ignore errors — user can manually open the URL
})
if (process.platform === 'win32') {
// PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not.
execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], () => {})
} else {
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open'
execFile(cmd, [url], () => {})
}
}
type WritableLike = Pick<typeof process.stderr, 'write'>