diff --git a/CHANGELOG.md b/CHANGELOG.md index 16e5acc2a..e0cc4b88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index b60511ba8..e17ca92de 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -217,6 +217,19 @@ export async function openUrl(url: string): Promise { } } +export async function openUrlInBackground(url: string): Promise { + 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, diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 09240bb55..2ef87f73f 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -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.", ); } diff --git a/src/wizard/onboarding.test.ts b/src/wizard/onboarding.test.ts index 1a01196eb..4cbae643f 100644 --- a/src/wizard/onboarding.test.ts +++ b/src/wizard/onboarding.test.ts @@ -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;