diff --git a/setup/channels/discord.ts b/setup/channels/discord.ts index 1dec8e008..435956f8f 100644 --- a/setup/channels/discord.ts +++ b/setup/channels/discord.ts @@ -164,9 +164,8 @@ async function walkThroughBotCreation(): Promise { ' 2. In the "Bot" tab, click "Reset Token" and copy the token', ' 3. On the same tab, enable "Message Content Intent"', ' (under Privileged Gateway Intents)', - '', formatNoteLink(url), - ].join('\n'), + ].filter((line): line is string => line !== null).join('\n'), 'Create a Discord bot', ); await confirmThenOpen(url, 'Press Enter to open the Developer Portal'); @@ -224,9 +223,8 @@ async function walkThroughServerCreation(): Promise { ' 1. In Discord, click the "+" at the bottom of the server list', ' 2. Choose "Create My Own" → "For me and my friends"', ' 3. Give it any name (e.g. "NanoClaw")', - '', formatNoteLink(url), - ].join('\n'), + ].filter((line): line is string => line !== null).join('\n'), 'Create a Discord server', ); await confirmThenOpen(url, 'Press Enter to open Discord'); @@ -446,9 +444,8 @@ async function promptInviteBot( '', ' 1. Pick any server you\'re in (a personal one is fine)', ' 2. Click "Authorize"', - '', formatNoteLink(url), - ].join('\n'), + ].filter((line): line is string => line !== null).join('\n'), 'Add bot to a server', ); await confirmThenOpen(url, 'Press Enter to open the invite page'); diff --git a/setup/channels/slack.ts b/setup/channels/slack.ts index 03cbf4656..24a10ce8d 100644 --- a/setup/channels/slack.ts +++ b/setup/channels/slack.ts @@ -135,9 +135,8 @@ async function walkThroughAppCreation(): Promise { ' slash commands and messages from the messages tab"', ' 4. Basic Information → copy the "Signing Secret"', ' 5. Install to Workspace → copy the "Bot User OAuth Token" (xoxb-…)', - '', formatNoteLink(SLACK_APPS_URL), - ].join('\n'), + ].filter((line): line is string => line !== null).join('\n'), 'Create a Slack app', ); await confirmThenOpen(SLACK_APPS_URL, 'Press Enter to open Slack app settings'); diff --git a/setup/channels/telegram.ts b/setup/channels/telegram.ts index 7130f8b5f..799a97f1b 100644 --- a/setup/channels/telegram.ts +++ b/setup/channels/telegram.ts @@ -50,9 +50,8 @@ export async function runTelegramChannel(displayName: string): Promise { note( [ `Opening @${botUsername} in Telegram so it's ready when the pairing code shows up.`, - '', formatNoteLink(botUrl), - ].join('\n'), + ].filter((line): line is string => line !== null).join('\n'), 'Open Telegram', ); await confirmThenOpen(botUrl, 'Press Enter to open Telegram'); diff --git a/setup/lib/browser.ts b/setup/lib/browser.ts index 4fbcbd7c3..7c5c970de 100644 --- a/setup/lib/browser.ts +++ b/setup/lib/browser.ts @@ -40,35 +40,42 @@ export function openUrl(url: string): void { } /** - * Format a URL for display inside a setup `note(...)` card. On - * GUI devices the URL renders dim — it's a fallback in case the - * auto-open misses, and `confirmThenOpen` is doing the heavy - * lifting of getting the user there. On headless devices the - * URL becomes the user's only path forward, so we surface it - * with a "Get started:" label and full-strength text — copy- - * pasting onto another device is the actual action, not an - * incidental reference. + * Format a URL for inclusion in a setup `note(...)` card. On + * headless devices we surface the URL inside the card with a + * "Get started:" label at full strength — copy-pasting onto + * another device is the actual action, not an incidental + * reference. The leading `\n` acts as a visual separator from + * the body steps above; callers `.filter(line => line !== null)` + * before joining, so on GUI we drop the line entirely (and the + * URL ends up below the next-step confirm prompt as a "if + * browser does not appear, please visit" fallback — see + * `confirmThenOpen`). */ -export function formatNoteLink(url: string): string { - if (isHeadless()) return `Get started: ${url}`; - return k.dim(url); +export function formatNoteLink(url: string): string | null { + if (isHeadless()) return `\nGet started: ${url}`; + return null; } /** * Gate a browser-open on a confirm so the user is ready for their browser - * to take focus. Proceeds on cancel as well — the user can always copy the - * URL from the note that precedes the prompt. On headless devices both - * the prompt and the open are skipped — there's no browser to time - * focus for, and the URL is already visible in the surrounding note. + * to take focus. Proceeds on cancel as well. On headless devices both the + * prompt and the open are skipped — the URL is already surfaced inside + * the surrounding note (via `formatNoteLink`). + * + * On GUI devices the confirm message includes the fallback URL on the + * lines below the action ("If browser does not appear, please visit: + * " in dim) so the user has a copy-paste path right next to the + * action button without needing to scroll back up to the card. */ export async function confirmThenOpen( url: string, message = 'Press Enter to open your browser', ): Promise { if (isHeadless()) return; + const fallback = `\n${k.dim(`If browser does not appear, please visit: ${url}`)}`; ensureAnswer( await p.confirm({ - message, + message: `${message}${fallback}`, initialValue: true, }), );