Merge pull request #2154 from alipgoldberg/setup-fallback-url-in-prompt

feat(setup): move URL fallback into the open-browser prompt
This commit is contained in:
gavrielc
2026-04-30 22:50:44 +03:00
committed by GitHub
4 changed files with 28 additions and 26 deletions

View File

@@ -164,9 +164,8 @@ async function walkThroughBotCreation(): Promise<void> {
' 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<void> {
' 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');

View File

@@ -135,9 +135,8 @@ async function walkThroughAppCreation(): Promise<void> {
' 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');

View File

@@ -50,9 +50,8 @@ export async function runTelegramChannel(displayName: string): Promise<void> {
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');

View File

@@ -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:
* <url>" 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<void> {
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,
}),
);