fix: relaunch setup tui in a fresh process

This commit is contained in:
Shakker
2026-04-20 04:24:04 +01:00
parent a112903802
commit 4aeb81eeeb
3 changed files with 78 additions and 7 deletions

70
src/tui/tui-launch.ts Normal file
View File

@@ -0,0 +1,70 @@
import { spawn } from "node:child_process";
import { formatErrorMessage } from "../infra/errors.js";
import { attachChildProcessBridge } from "../process/child-process-bridge.js";
import type { TuiOptions } from "./tui.js";
function appendOption(args: string[], flag: string, value: string | number | undefined): void {
if (value === undefined) {
return;
}
args.push(flag, String(value));
}
function buildTuiCliArgs(opts: TuiOptions): string[] {
const entry = process.argv[1]?.trim();
if (!entry) {
throw new Error("unable to relaunch TUI: current CLI entry path is unavailable");
}
const args = [...process.execArgv, entry, "tui"];
appendOption(args, "--url", opts.url);
appendOption(args, "--token", opts.token);
appendOption(args, "--password", opts.password);
appendOption(args, "--session", opts.session);
appendOption(args, "--thinking", opts.thinking);
appendOption(args, "--message", opts.message);
appendOption(args, "--timeout-ms", opts.timeoutMs);
appendOption(args, "--history-limit", opts.historyLimit);
if (opts.deliver) {
args.push("--deliver");
}
return args;
}
export async function launchTuiCli(opts: TuiOptions): Promise<void> {
const args = buildTuiCliArgs(opts);
const stdinWasPaused =
typeof process.stdin.isPaused === "function" ? process.stdin.isPaused() : false;
process.stdin.pause();
await new Promise<void>((resolve, reject) => {
const child = spawn(process.execPath, args, {
stdio: "inherit",
env: process.env,
});
const { detach } = attachChildProcessBridge(child);
child.once("error", (error) => {
detach();
reject(new Error(`failed to launch TUI: ${formatErrorMessage(error)}`));
});
child.once("exit", (code, signal) => {
detach();
if (signal) {
reject(new Error(`TUI exited from signal ${signal}`));
return;
}
if ((code ?? 0) !== 0) {
reject(new Error(`TUI exited with code ${code ?? 1}`));
return;
}
resolve();
});
}).finally(() => {
if (!stdinWasPaused) {
process.stdin.resume();
}
});
}

View File

@@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../config/config.js";
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
import type { RuntimeEnv } from "../runtime.js";
const runTui = vi.hoisted(() => vi.fn(async () => {}));
const launchTuiCli = vi.hoisted(() => vi.fn(async () => {}));
const probeGatewayReachable = vi.hoisted(() =>
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
);
@@ -137,8 +137,8 @@ vi.mock("../terminal/restore.js", () => ({
restoreTerminalState: vi.fn(),
}));
vi.mock("../tui/tui.js", () => ({
runTui,
vi.mock("../tui/tui-launch.js", () => ({
launchTuiCli,
}));
vi.mock("./setup.secret-input.js", () => ({
@@ -236,7 +236,7 @@ function createAdvancedFinalizeArgs(params: AdvancedFinalizeArgs = {}) {
describe("finalizeSetupWizard", () => {
beforeEach(() => {
runTui.mockClear();
launchTuiCli.mockClear();
probeGatewayReachable.mockClear();
waitForGatewayReachable.mockReset();
waitForGatewayReachable.mockResolvedValue({ ok: true });
@@ -337,7 +337,7 @@ describe("finalizeSetupWizard", () => {
password: "resolved-gateway-password", // pragma: allowlist secret
}),
);
expect(runTui).toHaveBeenCalledWith(
expect(launchTuiCli).toHaveBeenCalledWith(
expect.objectContaining({
url: "ws://127.0.0.1:18789",
password: "resolved-gateway-password", // pragma: allowlist secret

View File

@@ -30,7 +30,7 @@ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
import { formatErrorMessage } from "../infra/errors.js";
import type { RuntimeEnv } from "../runtime.js";
import { restoreTerminalState } from "../terminal/restore.js";
import { runTui } from "../tui/tui.js";
import { launchTuiCli } from "../tui/tui-launch.js";
import { resolveUserPath } from "../utils.js";
import { listConfiguredWebSearchProviders } from "../web-search/runtime.js";
import type { WizardPrompter } from "./prompts.js";
@@ -423,7 +423,7 @@ export async function finalizeSetupWizard(
if (hatchChoice === "tui") {
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
await runTui({
await launchTuiCli({
url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
@@ -431,6 +431,7 @@ export async function finalizeSetupWizard(
deliver: false,
message: hasBootstrap ? "Wake up, my friend!" : undefined,
});
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: true });
launchedTui = true;
} else if (hatchChoice === "web") {
const browserSupport = await detectBrowserOpenSupport();