From 873cee6947818f8047e50c57d5da2d6c532b5c57 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 05:08:07 +0000 Subject: [PATCH] feat: streamline wizard selection prompts --- src/commands/agents.ts | 18 ++-- src/commands/auth-choice-options.ts | 99 ++++++++++++++++- src/commands/auth-choice-prompt.ts | 63 +++++++++++ src/commands/configure.ts | 158 +++++++++++++++++----------- src/wizard/onboarding.ts | 17 ++- 5 files changed, 270 insertions(+), 85 deletions(-) create mode 100644 src/commands/auth-choice-prompt.ts diff --git a/src/commands/agents.ts b/src/commands/agents.ts index 72fa35e53..cc6d0eebc 100644 --- a/src/commands/agents.ts +++ b/src/commands/agents.ts @@ -35,10 +35,10 @@ import { resolveUserPath } from "../utils.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { WizardCancelledError } from "../wizard/prompts.js"; import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js"; -import { buildAuthChoiceOptions } from "./auth-choice-options.js"; +import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js"; import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js"; import { setupProviders } from "./onboard-providers.js"; -import type { AuthChoice, ProviderChoice } from "./onboard-types.js"; +import type { ProviderChoice } from "./onboard-types.js"; type AgentsListOptions = { json?: boolean; @@ -920,14 +920,12 @@ export async function agentsAddCommand( const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, }); - const authChoice = (await prompter.select({ - message: "Model/auth choice", - options: buildAuthChoiceOptions({ - store: authStore, - includeSkip: true, - includeClaudeCliIfMissing: true, - }), - })) as AuthChoice; + const authChoice = await promptAuthChoiceGrouped({ + prompter, + store: authStore, + includeSkip: true, + includeClaudeCliIfMissing: true, + }); const authResult = await applyAuthChoice({ authChoice, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 159a00753..dbc27d08b 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -12,6 +12,72 @@ export type AuthChoiceOption = { hint?: string; }; +export type AuthChoiceGroupId = + | "openai" + | "anthropic" + | "google" + | "openrouter" + | "zai" + | "opencode-zen" + | "minimax"; + +export type AuthChoiceGroup = { + value: AuthChoiceGroupId; + label: string; + hint?: string; + options: AuthChoiceOption[]; +}; + +const AUTH_CHOICE_GROUP_DEFS: { + value: AuthChoiceGroupId; + label: string; + hint?: string; + choices: AuthChoice[]; +}[] = [ + { + value: "openai", + label: "OpenAI", + hint: "Codex OAuth + API key", + choices: ["codex-cli", "openai-codex", "openai-api-key"], + }, + { + value: "anthropic", + label: "Anthropic", + hint: "Claude CLI + API key", + choices: ["claude-cli", "setup-token", "token", "apiKey"], + }, + { + value: "google", + label: "Google", + hint: "Antigravity + Gemini API key", + choices: ["antigravity", "gemini-api-key"], + }, + { + value: "openrouter", + label: "OpenRouter", + hint: "API key", + choices: ["openrouter-api-key"], + }, + { + value: "zai", + label: "Z.AI (GLM 4.7)", + hint: "API key", + choices: ["zai-api-key"], + }, + { + value: "opencode-zen", + label: "OpenCode Zen", + hint: "API key", + choices: ["opencode-zen"], + }, + { + value: "minimax", + label: "MiniMax", + hint: "Hosted + LM Studio + API", + choices: ["minimax-cloud", "minimax", "minimax-api"], + }, +]; + function formatOAuthHint( expires?: number, opts?: { allowStale?: boolean }, @@ -98,7 +164,7 @@ export function buildAuthChoiceOptions(params: { label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)", }); options.push({ value: "gemini-api-key", label: "Google Gemini API key" }); - options.push({ value: "zai-api-key", label: "Z.AI (GLM) API key" }); + options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }); options.push({ value: "apiKey", label: "Anthropic API key" }); // Token flow is currently Anthropic-only; use CLI for advanced providers. options.push({ @@ -118,3 +184,34 @@ export function buildAuthChoiceOptions(params: { return options; } + +export function buildAuthChoiceGroups(params: { + store: AuthProfileStore; + includeSkip: boolean; + includeClaudeCliIfMissing?: boolean; + platform?: NodeJS.Platform; +}): { + groups: AuthChoiceGroup[]; + skipOption?: AuthChoiceOption; +} { + const options = buildAuthChoiceOptions({ + ...params, + includeSkip: false, + }); + const optionByValue = new Map( + options.map((opt) => [opt.value, opt]), + ); + + const groups = AUTH_CHOICE_GROUP_DEFS.map((group) => ({ + ...group, + options: group.choices + .map((choice) => optionByValue.get(choice)) + .filter((opt): opt is AuthChoiceOption => Boolean(opt)), + })); + + const skipOption = params.includeSkip + ? ({ value: "skip", label: "Skip for now" } satisfies AuthChoiceOption) + : undefined; + + return { groups, skipOption }; +} diff --git a/src/commands/auth-choice-prompt.ts b/src/commands/auth-choice-prompt.ts new file mode 100644 index 000000000..57a4c7f76 --- /dev/null +++ b/src/commands/auth-choice-prompt.ts @@ -0,0 +1,63 @@ +import type { AuthProfileStore } from "../agents/auth-profiles.js"; +import type { WizardPrompter } from "../wizard/prompts.js"; +import { buildAuthChoiceGroups } from "./auth-choice-options.js"; +import type { AuthChoice } from "./onboard-types.js"; + +const BACK_VALUE = "__back"; + +export async function promptAuthChoiceGrouped(params: { + prompter: WizardPrompter; + store: AuthProfileStore; + includeSkip: boolean; + includeClaudeCliIfMissing?: boolean; + platform?: NodeJS.Platform; +}): Promise { + const { groups, skipOption } = buildAuthChoiceGroups(params); + const availableGroups = groups.filter((group) => group.options.length > 0); + + while (true) { + const providerOptions = [ + ...availableGroups.map((group) => ({ + value: group.value, + label: group.label, + hint: group.hint, + })), + ...(skipOption ? [skipOption] : []), + ]; + + const providerSelection = (await params.prompter.select({ + message: "Model/auth provider", + options: providerOptions, + })) as string; + + if (providerSelection === "skip") { + return "skip"; + } + + const group = availableGroups.find( + (candidate) => candidate.value === providerSelection, + ); + + if (!group || group.options.length === 0) { + await params.prompter.note( + "No auth methods available for that provider.", + "Model/auth choice", + ); + continue; + } + + const methodSelection = (await params.prompter.select({ + message: `${group.label} auth method`, + options: [ + ...group.options, + { value: BACK_VALUE, label: "Back" }, + ], + })) as string; + + if (methodSelection === BACK_VALUE) { + continue; + } + + return methodSelection as AuthChoice; + } +} diff --git a/src/commands/configure.ts b/src/commands/configure.ts index 835107fe1..8914300dd 100644 --- a/src/commands/configure.ts +++ b/src/commands/configure.ts @@ -3,7 +3,6 @@ import path from "node:path"; import { confirm as clackConfirm, intro as clackIntro, - multiselect as clackMultiselect, outro as clackOutro, select as clackSelect, text as clackText, @@ -41,7 +40,7 @@ import { applyAuthChoice, resolvePreferredProviderForAuthChoice, } from "./auth-choice.js"; -import { buildAuthChoiceOptions } from "./auth-choice-options.js"; +import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, @@ -64,7 +63,6 @@ import { import { setupProviders } from "./onboard-providers.js"; import { promptRemoteGatewayConfig } from "./onboard-remote.js"; import { setupSkills } from "./onboard-skills.js"; -import type { AuthChoice } from "./onboard-types.js"; import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js"; export const CONFIGURE_WIZARD_SECTIONS = [ @@ -110,16 +108,92 @@ const select = (params: Parameters>[0]) => : { ...opt, hint: stylePromptHint(opt.hint) }, ), }); -const multiselect = (params: Parameters>[0]) => - clackMultiselect({ - ...params, - message: stylePromptMessage(params.message), - options: params.options.map((opt) => - opt.hint === undefined - ? opt - : { ...opt, hint: stylePromptHint(opt.hint) }, - ), - }); + +const CONFIGURE_SECTION_OPTIONS: { + value: WizardSection; + label: string; + hint: string; +}[] = [ + { + value: "workspace", + label: "Workspace", + hint: "Set default workspace + ensure sessions", + }, + { + value: "model", + label: "Model/auth", + hint: "Pick model + auth profile sources", + }, + { + value: "gateway", + label: "Gateway config", + hint: "Port/bind/auth/control UI settings", + }, + { + value: "daemon", + label: "Gateway daemon", + hint: "Install/manage the background service", + }, + { + value: "providers", + label: "Providers", + hint: "Link WhatsApp/Telegram/etc and defaults", + }, + { + value: "skills", + label: "Skills", + hint: "Install/enable workspace skills", + }, + { + value: "health", + label: "Health check", + hint: "Run gateway + provider checks", + }, +]; + +async function promptConfigureSections( + runtime: RuntimeEnv, +): Promise { + const selected: WizardSection[] = []; + let remaining = CONFIGURE_SECTION_OPTIONS.slice(); + let addMore = true; + + while (addMore && remaining.length > 0) { + const choice = guardCancel( + await select({ + message: + selected.length === 0 + ? "Select a section to configure" + : "Select another section to configure", + options: remaining, + initialValue: remaining[0]?.value, + }), + runtime, + ) as WizardSection; + + if (!selected.includes(choice)) { + selected.push(choice); + } + + remaining = CONFIGURE_SECTION_OPTIONS.filter( + (option) => !selected.includes(option.value), + ); + + if (remaining.length === 0) { + break; + } + + addMore = guardCancel( + await confirm({ + message: "Configure another section?", + initialValue: false, + }), + runtime, + ); + } + + return selected; +} async function promptGatewayConfig( cfg: ClawdbotConfig, @@ -294,15 +368,13 @@ async function promptAuthConfig( runtime: RuntimeEnv, prompter: WizardPrompter, ): Promise { - const authChoice: AuthChoice = await prompter.select({ - message: "Model/auth choice", - options: buildAuthChoiceOptions({ - store: ensureAuthProfileStore(undefined, { - allowKeychainPrompt: false, - }), - includeSkip: true, - includeClaudeCliIfMissing: true, + const authChoice = await promptAuthChoiceGrouped({ + prompter, + store: ensureAuthProfileStore(undefined, { + allowKeychainPrompt: false, }), + includeSkip: true, + includeClaudeCliIfMissing: true, }); let next = cfg; @@ -596,49 +668,7 @@ export async function runConfigureWizard( const selected = opts.sections ? opts.sections - : (guardCancel( - await multiselect({ - message: "Select sections to configure", - options: [ - { - value: "workspace", - label: "Workspace", - hint: "Set default workspace + ensure sessions", - }, - { - value: "model", - label: "Model/auth", - hint: "Pick model + auth profile sources", - }, - { - value: "gateway", - label: "Gateway config", - hint: "Port/bind/auth/control UI settings", - }, - { - value: "daemon", - label: "Gateway daemon", - hint: "Install/manage the background service", - }, - { - value: "providers", - label: "Providers", - hint: "Link WhatsApp/Telegram/etc and defaults", - }, - { - value: "skills", - label: "Skills", - hint: "Install/enable workspace skills", - }, - { - value: "health", - label: "Health check", - hint: "Run gateway + provider checks", - }, - ], - }), - runtime, - ) as WizardSection[]); + : await promptConfigureSections(runtime); if (!selected || selected.length === 0) { outro("No changes selected."); diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index d1b811599..d03f2b179 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -7,7 +7,7 @@ import { resolvePreferredProviderForAuthChoice, warnIfModelConfigLooksOff, } from "../commands/auth-choice.js"; -import { buildAuthChoiceOptions } from "../commands/auth-choice-options.js"; +import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js"; import { DEFAULT_GATEWAY_DAEMON_RUNTIME, GATEWAY_DAEMON_RUNTIME_OPTIONS, @@ -37,7 +37,6 @@ import { setupProviders } from "../commands/onboard-providers.js"; import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; import { setupSkills } from "../commands/onboard-skills.js"; import type { - AuthChoice, GatewayAuthChoice, OnboardMode, OnboardOptions, @@ -333,14 +332,12 @@ export async function runOnboardingWizard( const authChoiceFromPrompt = opts.authChoice === undefined; const authChoice = opts.authChoice ?? - ((await prompter.select({ - message: "Model/auth choice", - options: buildAuthChoiceOptions({ - store: authStore, - includeSkip: true, - includeClaudeCliIfMissing: true, - }), - })) as AuthChoice); + (await promptAuthChoiceGrouped({ + prompter, + store: authStore, + includeSkip: true, + includeClaudeCliIfMissing: true, + })); const authResult = await applyAuthChoice({ authChoice,