Onboarding/TUI: prevent prompt overlap and auto-open
- Stop onboarding output once TUI launches - Avoid background Web UI open on TUI path - Restore terminal state on exit - Add terminal restore helper
This commit is contained in:
@@ -16,6 +16,8 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Onboarding: keep TUI flow exclusive (skip completion prompt + background Web UI seed).
|
||||
- TUI: block onboarding output while TUI is active and restore terminal state on exit.
|
||||
- Agents: repair malformed tool calls and session transcripts. (#7473) Thanks @justinhuangcode.
|
||||
- fix(agents): validate AbortSignal instances before calling AbortSignal.any() (#7277) (thanks @Elarwei001)
|
||||
- fix(webchat): respect user scroll position during streaming and refresh (#7226) (thanks @marcomarandiz)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { OnboardOptions } from "./onboard-types.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { restoreTerminalState } from "../terminal/restore.js";
|
||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||
import { runOnboardingWizard } from "../wizard/onboarding.js";
|
||||
import { WizardCancelledError } from "../wizard/prompts.js";
|
||||
@@ -18,5 +19,7 @@ export async function runInteractiveOnboarding(
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
restoreTerminalState("onboarding finish");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { clearActiveProgressLine } from "./terminal/progress-line.js";
|
||||
import { restoreTerminalState } from "./terminal/restore.js";
|
||||
|
||||
export type RuntimeEnv = {
|
||||
log: typeof console.log;
|
||||
@@ -16,6 +17,7 @@ export const defaultRuntime: RuntimeEnv = {
|
||||
console.error(...args);
|
||||
},
|
||||
exit: (code) => {
|
||||
restoreTerminalState("runtime exit");
|
||||
process.exit(code);
|
||||
throw new Error("unreachable"); // satisfies tests when mocked
|
||||
},
|
||||
|
||||
49
src/terminal/restore.ts
Normal file
49
src/terminal/restore.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { clearActiveProgressLine } from "./progress-line.js";
|
||||
|
||||
const RESET_SEQUENCE = "\x1b[0m\x1b[?25h\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?2004l";
|
||||
|
||||
function reportRestoreFailure(scope: string, err: unknown, reason?: string): void {
|
||||
const suffix = reason ? ` (${reason})` : "";
|
||||
const message = `[terminal] restore ${scope} failed${suffix}: ${String(err)}`;
|
||||
try {
|
||||
process.stderr.write(`${message}\n`);
|
||||
} catch (writeErr) {
|
||||
try {
|
||||
console.error(`[terminal] restore reporting failed${suffix}: ${String(writeErr)}`);
|
||||
} catch (consoleErr) {
|
||||
throw consoleErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function restoreTerminalState(reason?: string): void {
|
||||
try {
|
||||
clearActiveProgressLine();
|
||||
} catch (err) {
|
||||
reportRestoreFailure("progress line", err, reason);
|
||||
}
|
||||
|
||||
const stdin = process.stdin;
|
||||
if (stdin.isTTY && typeof stdin.setRawMode === "function") {
|
||||
try {
|
||||
stdin.setRawMode(false);
|
||||
} catch (err) {
|
||||
reportRestoreFailure("raw mode", err, reason);
|
||||
}
|
||||
if (typeof stdin.isPaused === "function" && stdin.isPaused()) {
|
||||
try {
|
||||
stdin.resume();
|
||||
} catch (err) {
|
||||
reportRestoreFailure("stdin resume", err, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.stdout.isTTY) {
|
||||
try {
|
||||
process.stdout.write(RESET_SEQUENCE);
|
||||
} catch (err) {
|
||||
reportRestoreFailure("stdout reset", err, reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,4 +662,10 @@ export async function runTui(opts: TuiOptions) {
|
||||
updateFooter();
|
||||
tui.start();
|
||||
client.start();
|
||||
await new Promise<void>((resolve) => {
|
||||
const finish = () => resolve();
|
||||
process.once("exit", finish);
|
||||
process.once("SIGINT", finish);
|
||||
process.once("SIGTERM", finish);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
detectBrowserOpenSupport,
|
||||
formatControlUiSshHint,
|
||||
openUrl,
|
||||
openUrlInBackground,
|
||||
probeGatewayReachable,
|
||||
waitForGatewayReachable,
|
||||
resolveControlUiLinks,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
import { resolveGatewayService } from "../daemon/service.js";
|
||||
import { isSystemdUserServiceAvailable } from "../daemon/systemd.js";
|
||||
import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
||||
import { restoreTerminalState } from "../terminal/restore.js";
|
||||
import { runTui } from "../tui/tui.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
|
||||
@@ -43,7 +43,9 @@ type FinalizeOnboardingOptions = {
|
||||
runtime: RuntimeEnv;
|
||||
};
|
||||
|
||||
export async function finalizeOnboardingWizard(options: FinalizeOnboardingOptions) {
|
||||
export async function finalizeOnboardingWizard(
|
||||
options: FinalizeOnboardingOptions,
|
||||
): Promise<{ launchedTui: boolean }> {
|
||||
const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options;
|
||||
|
||||
const withWizardProgress = async <T>(
|
||||
@@ -286,6 +288,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
||||
let controlUiOpenHint: string | undefined;
|
||||
let seededInBackground = false;
|
||||
let hatchChoice: "tui" | "web" | "later" | null = null;
|
||||
let launchedTui = false;
|
||||
|
||||
if (!opts.skipUi && gatewayProbe.ok) {
|
||||
if (hasBootstrap) {
|
||||
@@ -321,6 +324,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
||||
});
|
||||
|
||||
if (hatchChoice === "tui") {
|
||||
restoreTerminalState("pre-onboarding tui");
|
||||
await runTui({
|
||||
url: links.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
@@ -329,17 +333,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
||||
deliver: false,
|
||||
message: hasBootstrap ? "Wake up, my friend!" : undefined,
|
||||
});
|
||||
if (settings.authMode === "token" && settings.gatewayToken) {
|
||||
seededInBackground = await openUrlInBackground(authedUrl);
|
||||
}
|
||||
if (seededInBackground) {
|
||||
await prompter.note(
|
||||
`Web UI seeded in the background. Open later with: ${formatCliCommand(
|
||||
"openclaw dashboard --no-open",
|
||||
)}`,
|
||||
"Web UI",
|
||||
);
|
||||
}
|
||||
launchedTui = true;
|
||||
} else if (hatchChoice === "web") {
|
||||
const browserSupport = await detectBrowserOpenSupport();
|
||||
if (browserSupport.ok) {
|
||||
@@ -471,4 +465,6 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
||||
? "Onboarding complete. Web UI seeded in the background; open it anytime with the tokenized link above."
|
||||
: "Onboarding complete. Use the tokenized dashboard link above to control OpenClaw.",
|
||||
);
|
||||
|
||||
return { launchedTui };
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ export async function runOnboardingWizard(
|
||||
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
|
||||
await writeConfigFile(nextConfig);
|
||||
|
||||
await finalizeOnboardingWizard({
|
||||
const { launchedTui } = await finalizeOnboardingWizard({
|
||||
flow,
|
||||
opts,
|
||||
baseConfig,
|
||||
@@ -465,6 +465,9 @@ export async function runOnboardingWizard(
|
||||
prompter,
|
||||
runtime,
|
||||
});
|
||||
if (launchedTui) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installShell = await prompter.confirm({
|
||||
message: "Install shell completion script?",
|
||||
|
||||
Reference in New Issue
Block a user