feat: streamline wizard selection prompts

This commit is contained in:
Peter Steinberger
2026-01-12 05:08:07 +00:00
parent abdf4c30b2
commit 873cee6947
5 changed files with 270 additions and 85 deletions

View File

@@ -35,10 +35,10 @@ import { resolveUserPath } from "../utils.js";
import { createClackPrompter } from "../wizard/clack-prompter.js"; import { createClackPrompter } from "../wizard/clack-prompter.js";
import { WizardCancelledError } from "../wizard/prompts.js"; import { WizardCancelledError } from "../wizard/prompts.js";
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.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 { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
import { setupProviders } from "./onboard-providers.js"; import { setupProviders } from "./onboard-providers.js";
import type { AuthChoice, ProviderChoice } from "./onboard-types.js"; import type { ProviderChoice } from "./onboard-types.js";
type AgentsListOptions = { type AgentsListOptions = {
json?: boolean; json?: boolean;
@@ -920,14 +920,12 @@ export async function agentsAddCommand(
const authStore = ensureAuthProfileStore(agentDir, { const authStore = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false, allowKeychainPrompt: false,
}); });
const authChoice = (await prompter.select({ const authChoice = await promptAuthChoiceGrouped({
message: "Model/auth choice", prompter,
options: buildAuthChoiceOptions({ store: authStore,
store: authStore, includeSkip: true,
includeSkip: true, includeClaudeCliIfMissing: true,
includeClaudeCliIfMissing: true, });
}),
})) as AuthChoice;
const authResult = await applyAuthChoice({ const authResult = await applyAuthChoice({
authChoice, authChoice,

View File

@@ -12,6 +12,72 @@ export type AuthChoiceOption = {
hint?: string; 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( function formatOAuthHint(
expires?: number, expires?: number,
opts?: { allowStale?: boolean }, opts?: { allowStale?: boolean },
@@ -98,7 +164,7 @@ export function buildAuthChoiceOptions(params: {
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)", label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
}); });
options.push({ value: "gemini-api-key", label: "Google Gemini API key" }); 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" }); options.push({ value: "apiKey", label: "Anthropic API key" });
// Token flow is currently Anthropic-only; use CLI for advanced providers. // Token flow is currently Anthropic-only; use CLI for advanced providers.
options.push({ options.push({
@@ -118,3 +184,34 @@ export function buildAuthChoiceOptions(params: {
return options; 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<AuthChoice, AuthChoiceOption>(
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 };
}

View File

@@ -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<AuthChoice> {
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;
}
}

View File

@@ -3,7 +3,6 @@ import path from "node:path";
import { import {
confirm as clackConfirm, confirm as clackConfirm,
intro as clackIntro, intro as clackIntro,
multiselect as clackMultiselect,
outro as clackOutro, outro as clackOutro,
select as clackSelect, select as clackSelect,
text as clackText, text as clackText,
@@ -41,7 +40,7 @@ import {
applyAuthChoice, applyAuthChoice,
resolvePreferredProviderForAuthChoice, resolvePreferredProviderForAuthChoice,
} from "./auth-choice.js"; } from "./auth-choice.js";
import { buildAuthChoiceOptions } from "./auth-choice-options.js"; import { promptAuthChoiceGrouped } from "./auth-choice-prompt.js";
import { import {
DEFAULT_GATEWAY_DAEMON_RUNTIME, DEFAULT_GATEWAY_DAEMON_RUNTIME,
GATEWAY_DAEMON_RUNTIME_OPTIONS, GATEWAY_DAEMON_RUNTIME_OPTIONS,
@@ -64,7 +63,6 @@ import {
import { setupProviders } from "./onboard-providers.js"; import { setupProviders } from "./onboard-providers.js";
import { promptRemoteGatewayConfig } from "./onboard-remote.js"; import { promptRemoteGatewayConfig } from "./onboard-remote.js";
import { setupSkills } from "./onboard-skills.js"; import { setupSkills } from "./onboard-skills.js";
import type { AuthChoice } from "./onboard-types.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js"; import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
export const CONFIGURE_WIZARD_SECTIONS = [ export const CONFIGURE_WIZARD_SECTIONS = [
@@ -110,16 +108,92 @@ const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
: { ...opt, hint: stylePromptHint(opt.hint) }, : { ...opt, hint: stylePromptHint(opt.hint) },
), ),
}); });
const multiselect = <T>(params: Parameters<typeof clackMultiselect<T>>[0]) =>
clackMultiselect({ const CONFIGURE_SECTION_OPTIONS: {
...params, value: WizardSection;
message: stylePromptMessage(params.message), label: string;
options: params.options.map((opt) => hint: string;
opt.hint === undefined }[] = [
? opt {
: { ...opt, hint: stylePromptHint(opt.hint) }, 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<WizardSection[]> {
const selected: WizardSection[] = [];
let remaining = CONFIGURE_SECTION_OPTIONS.slice();
let addMore = true;
while (addMore && remaining.length > 0) {
const choice = guardCancel(
await select<WizardSection>({
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( async function promptGatewayConfig(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
@@ -294,15 +368,13 @@ async function promptAuthConfig(
runtime: RuntimeEnv, runtime: RuntimeEnv,
prompter: WizardPrompter, prompter: WizardPrompter,
): Promise<ClawdbotConfig> { ): Promise<ClawdbotConfig> {
const authChoice: AuthChoice = await prompter.select({ const authChoice = await promptAuthChoiceGrouped({
message: "Model/auth choice", prompter,
options: buildAuthChoiceOptions({ store: ensureAuthProfileStore(undefined, {
store: ensureAuthProfileStore(undefined, { allowKeychainPrompt: false,
allowKeychainPrompt: false,
}),
includeSkip: true,
includeClaudeCliIfMissing: true,
}), }),
includeSkip: true,
includeClaudeCliIfMissing: true,
}); });
let next = cfg; let next = cfg;
@@ -596,49 +668,7 @@ export async function runConfigureWizard(
const selected = opts.sections const selected = opts.sections
? opts.sections ? opts.sections
: (guardCancel( : await promptConfigureSections(runtime);
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[]);
if (!selected || selected.length === 0) { if (!selected || selected.length === 0) {
outro("No changes selected."); outro("No changes selected.");

View File

@@ -7,7 +7,7 @@ import {
resolvePreferredProviderForAuthChoice, resolvePreferredProviderForAuthChoice,
warnIfModelConfigLooksOff, warnIfModelConfigLooksOff,
} from "../commands/auth-choice.js"; } from "../commands/auth-choice.js";
import { buildAuthChoiceOptions } from "../commands/auth-choice-options.js"; import { promptAuthChoiceGrouped } from "../commands/auth-choice-prompt.js";
import { import {
DEFAULT_GATEWAY_DAEMON_RUNTIME, DEFAULT_GATEWAY_DAEMON_RUNTIME,
GATEWAY_DAEMON_RUNTIME_OPTIONS, GATEWAY_DAEMON_RUNTIME_OPTIONS,
@@ -37,7 +37,6 @@ import { setupProviders } from "../commands/onboard-providers.js";
import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js"; import { promptRemoteGatewayConfig } from "../commands/onboard-remote.js";
import { setupSkills } from "../commands/onboard-skills.js"; import { setupSkills } from "../commands/onboard-skills.js";
import type { import type {
AuthChoice,
GatewayAuthChoice, GatewayAuthChoice,
OnboardMode, OnboardMode,
OnboardOptions, OnboardOptions,
@@ -333,14 +332,12 @@ export async function runOnboardingWizard(
const authChoiceFromPrompt = opts.authChoice === undefined; const authChoiceFromPrompt = opts.authChoice === undefined;
const authChoice = const authChoice =
opts.authChoice ?? opts.authChoice ??
((await prompter.select({ (await promptAuthChoiceGrouped({
message: "Model/auth choice", prompter,
options: buildAuthChoiceOptions({ store: authStore,
store: authStore, includeSkip: true,
includeSkip: true, includeClaudeCliIfMissing: true,
includeClaudeCliIfMissing: true, }));
}),
})) as AuthChoice);
const authResult = await applyAuthChoice({ const authResult = await applyAuthChoice({
authChoice, authChoice,