Merge pull request #794 from roshanasingh4/fix/777-windows-openurl-quotes

Fix Antigravity OAuth login on Windows (quote URL for cmd start)
This commit is contained in:
Peter Steinberger
2026-01-13 04:28:11 +00:00
committed by GitHub
5 changed files with 78 additions and 5 deletions

View File

@@ -21,6 +21,7 @@
- TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow) - TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow)
- Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow) - Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow)
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm) - Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
- Onboarding: quote Windows browser URLs when launching via `cmd start` to preserve OAuth query params. (#794 — thanks @roshanasingh4)
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs. - Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
- Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras) - Telegram: respect account-scoped bindings when webhook mode is enabled. (#821 — thanks @gumadeiras)

View File

@@ -12,8 +12,8 @@ import {
isCloudCodeAssistFormatError, isCloudCodeAssistFormatError,
isCompactionFailureError, isCompactionFailureError,
isContextOverflowError, isContextOverflowError,
isMessagingToolDuplicate,
isFailoverErrorMessage, isFailoverErrorMessage,
isMessagingToolDuplicate,
normalizeTextForComparison, normalizeTextForComparison,
resolveBootstrapMaxChars, resolveBootstrapMaxChars,
sanitizeGoogleTurnOrdering, sanitizeGoogleTurnOrdering,

View File

@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { openUrl, resolveBrowserOpenCommand } from "./onboard-helpers.js";
const mocks = vi.hoisted(() => ({
runCommandWithTimeout: vi.fn(async () => ({
stdout: "",
stderr: "",
code: 0,
signal: null,
killed: false,
})),
}));
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: mocks.runCommandWithTimeout,
}));
describe("openUrl", () => {
it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const url =
"https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&redirect_uri=http%3A%2F%2Flocalhost";
const ok = await openUrl(url);
expect(ok).toBe(true);
expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1);
const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? [];
expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']);
expect(argv?.at(-1)).toBe(`"${url}"`);
expect(options).toMatchObject({
timeoutMs: 5_000,
windowsVerbatimArguments: true,
});
});
});
describe("resolveBrowserOpenCommand", () => {
it("marks win32 commands as quoteUrl=true", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const resolved = await resolveBrowserOpenCommand();
expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]);
expect(resolved.quoteUrl).toBe(true);
});
});

View File

@@ -134,9 +134,14 @@ type BrowserOpenCommand = {
argv: string[] | null; argv: string[] | null;
reason?: string; reason?: string;
command?: string; command?: string;
/**
* Whether the URL must be wrapped in quotes when appended to argv.
* Needed for Windows `cmd /c start` where `&` splits commands.
*/
quoteUrl?: boolean;
}; };
async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> { export async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
const platform = process.platform; const platform = process.platform;
const hasDisplay = Boolean( const hasDisplay = Boolean(
process.env.DISPLAY || process.env.WAYLAND_DISPLAY, process.env.DISPLAY || process.env.WAYLAND_DISPLAY,
@@ -151,7 +156,11 @@ async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
} }
if (platform === "win32") { if (platform === "win32") {
return { argv: ["cmd", "/c", "start", ""], command: "cmd" }; return {
argv: ["cmd", "/c", "start", ""],
command: "cmd",
quoteUrl: true,
};
} }
if (platform === "darwin") { if (platform === "darwin") {
@@ -223,9 +232,22 @@ function resolveSshTargetHint(): string {
export async function openUrl(url: string): Promise<boolean> { export async function openUrl(url: string): Promise<boolean> {
const resolved = await resolveBrowserOpenCommand(); const resolved = await resolveBrowserOpenCommand();
if (!resolved.argv) return false; if (!resolved.argv) return false;
const command = [...resolved.argv, url]; const quoteUrl = resolved.quoteUrl === true;
const command = [...resolved.argv];
if (quoteUrl) {
if (command.at(-1) === "") {
// Preserve the empty title token for `start` when using verbatim args.
command[command.length - 1] = '""';
}
command.push(`"${url}"`);
} else {
command.push(url);
}
try { try {
await runCommandWithTimeout(command, { timeoutMs: 5_000 }); await runCommandWithTimeout(command, {
timeoutMs: 5_000,
windowsVerbatimArguments: quoteUrl,
});
return true; return true;
} catch { } catch {
// ignore; we still print the URL for manual open // ignore; we still print the URL for manual open

View File

@@ -48,6 +48,7 @@ export type CommandOptions = {
cwd?: string; cwd?: string;
input?: string; input?: string;
env?: NodeJS.ProcessEnv; env?: NodeJS.ProcessEnv;
windowsVerbatimArguments?: boolean;
}; };
export async function runCommandWithTimeout( export async function runCommandWithTimeout(
@@ -59,6 +60,7 @@ export async function runCommandWithTimeout(
? { timeoutMs: optionsOrTimeout } ? { timeoutMs: optionsOrTimeout }
: optionsOrTimeout; : optionsOrTimeout;
const { timeoutMs, cwd, input, env } = options; const { timeoutMs, cwd, input, env } = options;
const { windowsVerbatimArguments } = options;
const hasInput = input !== undefined; const hasInput = input !== undefined;
// Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed. // Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed.
@@ -67,6 +69,7 @@ export async function runCommandWithTimeout(
stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"], stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"],
cwd, cwd,
env: env ? { ...process.env, ...env } : process.env, env: env ? { ...process.env, ...env } : process.env,
windowsVerbatimArguments,
}); });
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";