feat: refine onboarding hatch flow
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Docs: add /model allowlist troubleshooting note. (#1405)
|
- Docs: add /model allowlist troubleshooting note. (#1405)
|
||||||
- Docs: add per-message Gmail search example for gog. (#1220) Thanks @mbelinky.
|
- 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.
|
- 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).
|
- 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.
|
- Signal: add typing indicators and DM read receipts via signal-cli.
|
||||||
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
|
- MSTeams: add file uploads, adaptive cards, and attachment handling improvements. (#1410) Thanks @Evizero.
|
||||||
|
|||||||
@@ -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(
|
export async function ensureWorkspaceAndSessions(
|
||||||
workspaceDir: string,
|
workspaceDir: string,
|
||||||
runtime: RuntimeEnv,
|
runtime: RuntimeEnv,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
detectBrowserOpenSupport,
|
detectBrowserOpenSupport,
|
||||||
formatControlUiSshHint,
|
formatControlUiSshHint,
|
||||||
openUrl,
|
openUrl,
|
||||||
|
openUrlInBackground,
|
||||||
probeGatewayReachable,
|
probeGatewayReachable,
|
||||||
waitForGatewayReachable,
|
waitForGatewayReachable,
|
||||||
resolveControlUiLinks,
|
resolveControlUiLinks,
|
||||||
@@ -282,6 +283,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
"Control UI",
|
"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 (!opts.skipUi && gatewayProbe.ok) {
|
||||||
if (hasBootstrap) {
|
if (hasBootstrap) {
|
||||||
await prompter.note(
|
await prompter.note(
|
||||||
@@ -293,11 +299,27 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
"Start TUI (best option!)",
|
"Start TUI (best option!)",
|
||||||
);
|
);
|
||||||
const wantsTui = await prompter.confirm({
|
await prompter.note(
|
||||||
message: "Do you want to hatch your bot now?",
|
[
|
||||||
initialValue: true,
|
"Gateway token: shared auth for the Gateway + Control UI.",
|
||||||
});
|
"Stored in: ~/.clawdbot/clawdbot.json (gateway.auth.token) or CLAWDBOT_GATEWAY_TOKEN.",
|
||||||
if (wantsTui) {
|
"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({
|
await runTui({
|
||||||
url: links.wsUrl,
|
url: links.wsUrl,
|
||||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||||
@@ -306,6 +328,52 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
deliver: false,
|
deliver: false,
|
||||||
message: "Wake up, my friend!",
|
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 {
|
} else {
|
||||||
const browserSupport = await detectBrowserOpenSupport();
|
const browserSupport = await detectBrowserOpenSupport();
|
||||||
@@ -342,9 +410,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
);
|
);
|
||||||
|
|
||||||
const shouldOpenControlUi =
|
const shouldOpenControlUi =
|
||||||
!opts.skipUi && settings.authMode === "token" && Boolean(settings.gatewayToken);
|
!opts.skipUi &&
|
||||||
let controlUiOpened = false;
|
settings.authMode === "token" &&
|
||||||
let controlUiOpenHint: string | undefined;
|
Boolean(settings.gatewayToken) &&
|
||||||
|
hatchChoice === null;
|
||||||
if (shouldOpenControlUi) {
|
if (shouldOpenControlUi) {
|
||||||
const browserSupport = await detectBrowserOpenSupport();
|
const browserSupport = await detectBrowserOpenSupport();
|
||||||
if (browserSupport.ok) {
|
if (browserSupport.ok) {
|
||||||
@@ -406,9 +475,16 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
|
|||||||
"Web search (optional)",
|
"Web search (optional)",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await prompter.note(
|
||||||
|
'What now: https://clawd.bot/showcase ("What People Are Building").',
|
||||||
|
"What now",
|
||||||
|
);
|
||||||
|
|
||||||
await prompter.outro(
|
await prompter.outro(
|
||||||
controlUiOpened
|
controlUiOpened
|
||||||
? "Onboarding complete. Dashboard opened with your token; keep that tab to control Clawdbot."
|
? "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.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,19 +177,19 @@ describe("runOnboardingWizard", () => {
|
|||||||
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-onboard-"));
|
const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-onboard-"));
|
||||||
await fs.writeFile(path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME), "{}");
|
await fs.writeFile(path.join(workspaceDir, DEFAULT_BOOTSTRAP_FILENAME), "{}");
|
||||||
|
|
||||||
const confirm: WizardPrompter["confirm"] = vi.fn(async (opts) => {
|
const select: WizardPrompter["select"] = vi.fn(async (opts) => {
|
||||||
if (opts.message === "Do you want to hatch your bot now?") return true;
|
if (opts.message === "How do you want to hatch your bot?") return "tui";
|
||||||
return opts.initialValue ?? false;
|
return "quickstart";
|
||||||
});
|
});
|
||||||
|
|
||||||
const prompter: WizardPrompter = {
|
const prompter: WizardPrompter = {
|
||||||
intro: vi.fn(async () => {}),
|
intro: vi.fn(async () => {}),
|
||||||
outro: vi.fn(async () => {}),
|
outro: vi.fn(async () => {}),
|
||||||
note: vi.fn(async () => {}),
|
note: vi.fn(async () => {}),
|
||||||
select: vi.fn(async () => "quickstart"),
|
select,
|
||||||
multiselect: vi.fn(async () => []),
|
multiselect: vi.fn(async () => []),
|
||||||
text: vi.fn(async () => ""),
|
text: vi.fn(async () => ""),
|
||||||
confirm,
|
confirm: vi.fn(async () => false),
|
||||||
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
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;
|
const calls = (note as unknown as { mock: { calls: unknown[][] } }).mock.calls;
|
||||||
expect(calls.length).toBeGreaterThan(0);
|
expect(calls.length).toBeGreaterThan(0);
|
||||||
const lastCall = calls[calls.length - 1];
|
expect(calls.some((call) => call?.[1] === "Web search (optional)")).toBe(true);
|
||||||
expect(lastCall?.[1]).toBe("Web search (optional)");
|
|
||||||
} finally {
|
} finally {
|
||||||
if (prevBraveKey === undefined) {
|
if (prevBraveKey === undefined) {
|
||||||
delete process.env.BRAVE_API_KEY;
|
delete process.env.BRAVE_API_KEY;
|
||||||
|
|||||||
Reference in New Issue
Block a user