fix: always offer TUI hatch

This commit is contained in:
Peter Steinberger
2026-01-23 09:04:04 +00:00
parent 8aadcaa1bd
commit 03e8b7c4ba
2 changed files with 121 additions and 82 deletions

View File

@@ -299,99 +299,83 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
].join("\n"), ].join("\n"),
"Start TUI (best option!)", "Start TUI (best option!)",
); );
await prompter.note( }
[
"Gateway token: shared auth for the Gateway + Control UI.",
"Stored in: ~/.clawdbot/clawdbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.",
"Web UI stores a copy in this browser's localStorage (clawdbot.control.settings.v1).",
`Get the tokenized link anytime: ${formatCliCommand("clawdbot dashboard --no-open")}`,
].join("\n"),
"Token",
);
hatchChoice = (await prompter.select({ await prompter.note(
message: "How do you want to hatch your bot?", [
options: [ "Gateway token: shared auth for the Gateway + Control UI.",
{ value: "tui", label: "Hatch in TUI (recommended)" }, "Stored in: ~/.clawdbot/clawdbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.",
{ value: "web", label: "Open the Web UI" }, "Web UI stores a copy in this browser's localStorage (clawdbot.control.settings.v1).",
{ value: "later", label: "Do this later" }, `Get the tokenized link anytime: ${formatCliCommand("clawdbot dashboard --no-open")}`,
], ].join("\n"),
initialValue: "tui", "Token",
})) as "tui" | "web" | "later"; );
if (hatchChoice === "tui") { hatchChoice = (await prompter.select({
await runTui({ message: "How do you want to hatch your bot?",
url: links.wsUrl, options: [
token: settings.authMode === "token" ? settings.gatewayToken : undefined, { value: "tui", label: "Hatch in TUI (recommended)" },
password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "", { value: "web", label: "Open the Web UI" },
// Safety: onboarding TUI should not auto-deliver to lastProvider/lastTo. { value: "later", label: "Do this later" },
deliver: false, ],
message: "Wake up, my friend!", initialValue: "tui",
}); })) as "tui" | "web" | "later";
if (settings.authMode === "token" && settings.gatewayToken) {
seededInBackground = await openUrlInBackground(authedUrl); if (hatchChoice === "tui") {
} await runTui({
if (seededInBackground) { url: links.wsUrl,
await prompter.note( token: settings.authMode === "token" ? settings.gatewayToken : undefined,
`Web UI seeded in the background. Open later with: ${formatCliCommand( password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "",
"clawdbot dashboard --no-open", // Safety: onboarding TUI should not auto-deliver to lastProvider/lastTo.
)}`, deliver: false,
"Web UI", message: hasBootstrap ? "Wake up, my friend!" : undefined,
); });
} if (settings.authMode === "token" && settings.gatewayToken) {
} else if (hatchChoice === "web") { seededInBackground = await openUrlInBackground(authedUrl);
const browserSupport = await detectBrowserOpenSupport(); }
if (browserSupport.ok) { if (seededInBackground) {
controlUiOpened = await openUrl(authedUrl); await prompter.note(
if (!controlUiOpened) { `Web UI seeded in the background. Open later with: ${formatCliCommand(
controlUiOpenHint = formatControlUiSshHint({ "clawdbot dashboard --no-open",
port: settings.port, )}`,
basePath: controlUiBasePath, "Web UI",
token: settings.gatewayToken, );
}); }
} } else if (hatchChoice === "web") {
} else { const browserSupport = await detectBrowserOpenSupport();
if (browserSupport.ok) {
controlUiOpened = await openUrl(authedUrl);
if (!controlUiOpened) {
controlUiOpenHint = formatControlUiSshHint({ controlUiOpenHint = formatControlUiSshHint({
port: settings.port, port: settings.port,
basePath: controlUiBasePath, basePath: controlUiBasePath,
token: settings.gatewayToken, token: settings.gatewayToken,
}); });
} }
await prompter.note(
[
`Dashboard link (with token): ${authedUrl}`,
controlUiOpened
? "Opened in your browser. Keep that tab to control Clawdbot."
: "Copy/paste this URL in a browser on this machine to control Clawdbot.",
controlUiOpenHint,
]
.filter(Boolean)
.join("\n"),
"Dashboard ready",
);
} else { } else {
await prompter.note( controlUiOpenHint = formatControlUiSshHint({
`When you're ready: ${formatCliCommand("clawdbot dashboard --no-open")}`, port: settings.port,
"Later", basePath: controlUiBasePath,
); token: settings.gatewayToken,
});
} }
await prompter.note(
[
`Dashboard link (with token): ${authedUrl}`,
controlUiOpened
? "Opened in your browser. Keep that tab to control Clawdbot."
: "Copy/paste this URL in a browser on this machine to control Clawdbot.",
controlUiOpenHint,
]
.filter(Boolean)
.join("\n"),
"Dashboard ready",
);
} else { } else {
const browserSupport = await detectBrowserOpenSupport(); await prompter.note(
if (!browserSupport.ok) { `When you're ready: ${formatCliCommand("clawdbot dashboard --no-open")}`,
await prompter.note( "Later",
formatControlUiSshHint({ );
port: settings.port,
basePath: controlUiBasePath,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
}),
"Open Control UI",
);
} else {
await prompter.note(
"Opening Control UI automatically after onboarding (no extra prompts).",
"Open Control UI",
);
}
} }
} else if (opts.skipUi) { } else if (opts.skipUi) {
await prompter.note("Skipping Control UI/TUI prompts.", "Control UI"); await prompter.note("Skipping Control UI/TUI prompts.", "Control UI");

View File

@@ -227,6 +227,61 @@ describe("runOnboardingWizard", () => {
await fs.rm(workspaceDir, { recursive: true, force: true }); await fs.rm(workspaceDir, { recursive: true, force: true });
}); });
it("offers TUI hatch even without BOOTSTRAP.md", async () => {
runTui.mockClear();
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-onboard-"));
const select: WizardPrompter["select"] = vi.fn(async (opts) => {
if (opts.message === "How do you want to hatch your bot?") return "tui";
return "quickstart";
});
const prompter: WizardPrompter = {
intro: vi.fn(async () => {}),
outro: vi.fn(async () => {}),
note: vi.fn(async () => {}),
select,
multiselect: vi.fn(async () => []),
text: vi.fn(async () => ""),
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
await runOnboardingWizard(
{
acceptRisk: true,
flow: "quickstart",
mode: "local",
workspace: workspaceDir,
authChoice: "skip",
skipProviders: true,
skipSkills: true,
skipHealth: true,
installDaemon: false,
},
runtime,
prompter,
);
expect(runTui).toHaveBeenCalledWith(
expect.objectContaining({
deliver: false,
message: undefined,
}),
);
await fs.rm(workspaceDir, { recursive: true, force: true });
});
it("shows the web search hint at the end of onboarding", async () => { it("shows the web search hint at the end of onboarding", async () => {
const prevBraveKey = process.env.BRAVE_API_KEY; const prevBraveKey = process.env.BRAVE_API_KEY;
delete process.env.BRAVE_API_KEY; delete process.env.BRAVE_API_KEY;