feat: refine onboarding hatch flow

This commit is contained in:
Peter Steinberger
2026-01-23 04:32:13 +00:00
parent 64be2b2cd1
commit 5d0d9e6323
4 changed files with 105 additions and 16 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.clawd.bot
- Docs: add /model allowlist troubleshooting note. (#1405)
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
- UI: show per-session assistant identity in the Control UI. (#1420) Thanks @robbyczgw-cla.
- Onboarding: add hatch choice (TUI/Web/Later), token explainer, background dashboard seed on macOS, and showcase link.
- Onboarding: remove the run setup-token auth option (paste setup-token or reuse CLI creds instead).
- Signal: add typing indicators and DM read receipts via signal-cli.
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.

View File

@@ -217,6 +217,19 @@ export async function openUrl(url: string): Promise<boolean> {
}
}
export async function openUrlInBackground(url: string): Promise<boolean> {
if (process.platform !== "darwin") return false;
const resolved = await resolveBrowserOpenCommand();
if (!resolved.argv || resolved.command !== "open") return false;
const command = ["open", "-g", url];
try {
await runCommandWithTimeout(command, { timeoutMs: 5_000 });
return true;
} catch {
return false;
}
}
export async function ensureWorkspaceAndSessions(
workspaceDir: string,
runtime: RuntimeEnv,

View File

@@ -13,6 +13,7 @@ import {
detectBrowserOpenSupport,
formatControlUiSshHint,
openUrl,
openUrlInBackground,
probeGatewayReachable,
waitForGatewayReachable,
resolveControlUiLinks,
@@ -282,6 +283,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
"Control UI",
);
let controlUiOpened = false;
let controlUiOpenHint: string | undefined;
let seededInBackground = false;
let hatchChoice: "tui" | "web" | "later" | null = null;
if (!opts.skipUi && gatewayProbe.ok) {
if (hasBootstrap) {
await prompter.note(
@@ -293,11 +299,27 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
].join("\n"),
"Start TUI (best option!)",
);
const wantsTui = await prompter.confirm({
message: "Do you want to hatch your bot now?",
initialValue: true,
});
if (wantsTui) {
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({
message: "How do you want to hatch your bot?",
options: [
{ value: "tui", label: "Hatch in TUI (recommended)" },
{ value: "web", label: "Open the Web UI" },
{ value: "later", label: "Do this later" },
],
initialValue: "tui",
})) as "tui" | "web" | "later";
if (hatchChoice === "tui") {
await runTui({
url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
@@ -306,6 +328,52 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
deliver: false,
message: "Wake up, my friend!",
});
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(
"clawdbot dashboard --no-open",
)}`,
"Web UI",
);
}
} else if (hatchChoice === "web") {
const browserSupport = await detectBrowserOpenSupport();
if (browserSupport.ok) {
controlUiOpened = await openUrl(authedUrl);
if (!controlUiOpened) {
controlUiOpenHint = formatControlUiSshHint({
port: settings.port,
basePath: controlUiBasePath,
token: settings.gatewayToken,
});
}
} else {
controlUiOpenHint = formatControlUiSshHint({
port: settings.port,
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 {
await prompter.note(
`When you're ready: ${formatCliCommand("clawdbot dashboard --no-open")}`,
"Later",
);
}
} else {
const browserSupport = await detectBrowserOpenSupport();
@@ -342,9 +410,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
);
const shouldOpenControlUi =
!opts.skipUi && settings.authMode === "token" && Boolean(settings.gatewayToken);
let controlUiOpened = false;
let controlUiOpenHint: string | undefined;
!opts.skipUi &&
settings.authMode === "token" &&
Boolean(settings.gatewayToken) &&
hatchChoice === null;
if (shouldOpenControlUi) {
const browserSupport = await detectBrowserOpenSupport();
if (browserSupport.ok) {
@@ -406,9 +475,16 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
"Web search (optional)",
);
await prompter.note(
'What now: https://clawd.bot/showcase ("What People Are Building").',
"What now",
);
await prompter.outro(
controlUiOpened
? "Onboarding complete. Dashboard opened with your token; keep that tab to control Clawdbot."
: "Onboarding complete. Use the tokenized dashboard link above to control Clawdbot.",
: seededInBackground
? "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 Clawdbot.",
);
}

View File

@@ -177,19 +177,19 @@ describe("runOnboardingWizard", () => {
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-onboard-"));
await fs.writeFile(path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME), "{}");
const confirm: WizardPrompter["confirm"] = vi.fn(async (opts) => {
if (opts.message === "Do you want to hatch your bot now?") return true;
return opts.initialValue ?? false;
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: vi.fn(async () => "quickstart"),
select,
multiselect: vi.fn(async () => []),
text: vi.fn(async () => ""),
confirm,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
};
@@ -267,8 +267,7 @@ describe("runOnboardingWizard", () => {
const calls = (note as unknown as { mock: { calls: unknown[][] } }).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const lastCall = calls[calls.length - 1];
expect(lastCall?.[1]).toBe("Web search (optional)");
expect(calls.some((call) => call?.[1] === "Web search (optional)")).toBe(true);
} finally {
if (prevBraveKey === undefined) {
delete process.env.BRAVE_API_KEY;