mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-06-04 10:14:47 +08:00
feat(setup): move URL fallback into the open-browser prompt
On GUI devices the URL was previously rendered dim inside the
instructional `note(...)` card, then `confirmThenOpen` printed
its prompt below: read the card, see the URL, then a separate
"Press Enter to open the X" prompt with no link near it. Two
visual moments for what's really one decision.
This PR pulls the URL out of the card on GUI devices and
relocates it directly under the action line of the confirm
prompt, separated only by a dim "If browser does not appear,
please visit: <url>" line:
│
◆ Press Enter to open the Developer Portal
│ If browser does not appear, please visit: … (dim)
│ ● Yes / ○ No
│
Action and fallback live as one prompt block — the user sees
both at the same time, no need to scroll back up to grab the
URL if the auto-open misses.
Headless behavior is unchanged: `formatNoteLink` still emits
"Get started: <url>" inside the card on headless devices (per
#2146), and `confirmThenOpen` still no-ops on headless (per
#2145). The only thing that changed for headless is the leading
`\n` in the helper output, which acts as a visual separator from
the steps above.
Five call sites adjusted (Discord ×3, Slack ×1, Telegram ×1) to
use `.filter((line) => line !== null)` so the now-nullable
`formatNoteLink` cleanly drops out of GUI-rendered cards.
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
+23
-16
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user