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 { 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,

View File

@@ -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<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 {
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 = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
: { ...opt, hint: stylePromptHint(opt.hint) },
),
});
const multiselect = <T>(params: Parameters<typeof clackMultiselect<T>>[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<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(
cfg: ClawdbotConfig,
@@ -294,15 +368,13 @@ async function promptAuthConfig(
runtime: RuntimeEnv,
prompter: WizardPrompter,
): Promise<ClawdbotConfig> {
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.");

View File

@@ -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,