refactor: expand setup wizard input flow

This commit is contained in:
Peter Steinberger 2026-03-15 17:06:15 -07:00
parent 70a228cdaa
commit c6239bf253
No known key found for this signature in database

View File

@ -3,6 +3,7 @@ import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import type { WizardPrompter } from "../../wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingConfigureContext,
ChannelOnboardingDmPolicy,
ChannelOnboardingStatus,
ChannelOnboardingStatusContext,
@ -26,6 +27,18 @@ export type ChannelSetupWizardStatus = {
configuredScore?: number;
unconfiguredScore?: number;
resolveConfigured: (params: { cfg: OpenClawConfig }) => boolean | Promise<boolean>;
resolveStatusLines?: (params: {
cfg: OpenClawConfig;
configured: boolean;
}) => string[] | Promise<string[]>;
resolveSelectionHint?: (params: {
cfg: OpenClawConfig;
configured: boolean;
}) => string | undefined | Promise<string | undefined>;
resolveQuickstartScore?: (params: {
cfg: OpenClawConfig;
configured: boolean;
}) => number | undefined | Promise<number | undefined>;
};
export type ChannelSetupWizardCredentialState = {
@ -84,6 +97,51 @@ export type ChannelSetupWizardCredential = {
}) => OpenClawConfig | Promise<OpenClawConfig>;
};
export type ChannelSetupWizardTextInput = {
inputKey: keyof ChannelSetupInput;
message: string;
placeholder?: string;
required?: boolean;
helpTitle?: string;
helpLines?: string[];
confirmCurrentValue?: boolean;
keepPrompt?: string | ((value: string) => string);
currentValue?: (params: {
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
}) => string | undefined | Promise<string | undefined>;
initialValue?: (params: {
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
}) => string | undefined | Promise<string | undefined>;
shouldPrompt?: (params: {
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
currentValue?: string;
}) => boolean | Promise<boolean>;
applyCurrentValue?: boolean;
validate?: (params: {
value: string;
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
}) => string | undefined;
normalizeValue?: (params: {
value: string;
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
}) => string;
applySet?: (params: {
cfg: OpenClawConfig;
accountId: string;
value: string;
}) => OpenClawConfig | Promise<OpenClawConfig>;
};
export type ChannelSetupWizardAllowFromEntry = {
input: string;
resolved: boolean;
@ -139,12 +197,33 @@ export type ChannelSetupWizardGroupAccess = {
}) => OpenClawConfig;
};
export type ChannelSetupWizardPrepare = (params: {
cfg: OpenClawConfig;
accountId: string;
credentialValues: ChannelSetupWizardCredentialValues;
runtime: ChannelOnboardingConfigureContext["runtime"];
prompter: WizardPrompter;
options?: ChannelOnboardingConfigureContext["options"];
}) =>
| {
cfg?: OpenClawConfig;
credentialValues?: ChannelSetupWizardCredentialValues;
}
| void
| Promise<{
cfg?: OpenClawConfig;
credentialValues?: ChannelSetupWizardCredentialValues;
} | void>;
export type ChannelSetupWizard = {
channel: string;
status: ChannelSetupWizardStatus;
introNote?: ChannelSetupWizardNote;
envShortcut?: ChannelSetupWizardEnvShortcut;
prepare?: ChannelSetupWizardPrepare;
credentials: ChannelSetupWizardCredential[];
textInputs?: ChannelSetupWizardTextInput[];
completionNote?: ChannelSetupWizardNote;
dmPolicy?: ChannelOnboardingDmPolicy;
allowFrom?: ChannelSetupWizardAllowFrom;
groupAccess?: ChannelSetupWizardGroupAccess;
@ -160,14 +239,28 @@ async function buildStatus(
ctx: ChannelOnboardingStatusContext,
): Promise<ChannelOnboardingStatus> {
const configured = await wizard.status.resolveConfigured({ cfg: ctx.cfg });
const statusLines = (await wizard.status.resolveStatusLines?.({
cfg: ctx.cfg,
configured,
})) ?? [
`${plugin.meta.label}: ${configured ? wizard.status.configuredLabel : wizard.status.unconfiguredLabel}`,
];
const selectionHint =
(await wizard.status.resolveSelectionHint?.({
cfg: ctx.cfg,
configured,
})) ?? (configured ? wizard.status.configuredHint : wizard.status.unconfiguredHint);
const quickstartScore =
(await wizard.status.resolveQuickstartScore?.({
cfg: ctx.cfg,
configured,
})) ?? (configured ? wizard.status.configuredScore : wizard.status.unconfiguredScore);
return {
channel: plugin.id,
configured,
statusLines: [
`${plugin.meta.label}: ${configured ? wizard.status.configuredLabel : wizard.status.unconfiguredLabel}`,
],
selectionHint: configured ? wizard.status.configuredHint : wizard.status.unconfiguredHint,
quickstartScore: configured ? wizard.status.configuredScore : wizard.status.unconfiguredScore,
statusLines,
selectionHint,
quickstartScore,
};
}
@ -238,6 +331,29 @@ function collectCredentialValues(params: {
return values;
}
async function applyWizardTextInputValue(params: {
plugin: ChannelSetupWizardPlugin;
input: ChannelSetupWizardTextInput;
cfg: OpenClawConfig;
accountId: string;
value: string;
}) {
return params.input.applySet
? await params.input.applySet({
cfg: params.cfg,
accountId: params.accountId,
value: params.value,
})
: applySetupInput({
plugin: params.plugin,
cfg: params.cfg,
accountId: params.accountId,
input: {
[params.input.inputKey]: params.value,
},
}).cfg;
}
export function buildChannelOnboardingAdapterFromSetupWizard(params: {
plugin: ChannelSetupWizardPlugin;
wizard: ChannelSetupWizard;
@ -248,6 +364,7 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: {
getStatus: async (ctx) => buildStatus(plugin, wizard, ctx),
configure: async ({
cfg,
runtime,
prompter,
options,
accountOverrides,
@ -305,6 +422,26 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: {
await prompter.note(wizard.introNote.lines.join("\n"), wizard.introNote.title);
}
if (wizard.prepare) {
const prepared = await wizard.prepare({
cfg: next,
accountId,
credentialValues,
runtime,
prompter,
options,
});
if (prepared?.cfg) {
next = prepared.cfg;
}
if (prepared?.credentialValues) {
credentialValues = {
...credentialValues,
...prepared.credentialValues,
};
}
}
if (!usedEnvShortcut) {
for (const credential of wizard.credentials) {
let credentialState = credential.inspect({ cfg: next, accountId });
@ -383,6 +520,129 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: {
}
}
for (const textInput of wizard.textInputs ?? []) {
let currentValue = trimResolvedValue(
typeof credentialValues[textInput.inputKey] === "string"
? credentialValues[textInput.inputKey]
: undefined,
);
if (!currentValue && textInput.currentValue) {
currentValue = trimResolvedValue(
await textInput.currentValue({
cfg: next,
accountId,
credentialValues,
}),
);
}
const shouldPrompt = textInput.shouldPrompt
? await textInput.shouldPrompt({
cfg: next,
accountId,
credentialValues,
currentValue,
})
: true;
if (!shouldPrompt) {
if (currentValue) {
credentialValues[textInput.inputKey] = currentValue;
if (textInput.applyCurrentValue) {
next = await applyWizardTextInputValue({
plugin,
input: textInput,
cfg: next,
accountId,
value: currentValue,
});
}
}
continue;
}
if (textInput.helpLines && textInput.helpLines.length > 0) {
await prompter.note(
textInput.helpLines.join("\n"),
textInput.helpTitle ?? textInput.message,
);
}
if (currentValue && textInput.confirmCurrentValue !== false) {
const keep = await prompter.confirm({
message:
typeof textInput.keepPrompt === "function"
? textInput.keepPrompt(currentValue)
: (textInput.keepPrompt ?? `${textInput.message} set (${currentValue}). Keep it?`),
initialValue: true,
});
if (keep) {
credentialValues[textInput.inputKey] = currentValue;
if (textInput.applyCurrentValue) {
next = await applyWizardTextInputValue({
plugin,
input: textInput,
cfg: next,
accountId,
value: currentValue,
});
}
continue;
}
}
const initialValue = trimResolvedValue(
(await textInput.initialValue?.({
cfg: next,
accountId,
credentialValues,
})) ?? currentValue,
);
const rawValue = String(
await prompter.text({
message: textInput.message,
initialValue,
placeholder: textInput.placeholder,
validate: (value) => {
const trimmed = String(value ?? "").trim();
if (!trimmed && textInput.required !== false) {
return "Required";
}
return textInput.validate?.({
value: trimmed,
cfg: next,
accountId,
credentialValues,
});
},
}),
);
const trimmedValue = rawValue.trim();
if (!trimmedValue && textInput.required === false) {
delete credentialValues[textInput.inputKey];
continue;
}
const normalizedValue = trimResolvedValue(
textInput.normalizeValue?.({
value: trimmedValue,
cfg: next,
accountId,
credentialValues,
}) ?? trimmedValue,
);
if (!normalizedValue) {
delete credentialValues[textInput.inputKey];
continue;
}
next = await applyWizardTextInputValue({
plugin,
input: textInput,
cfg: next,
accountId,
value: normalizedValue,
});
credentialValues[textInput.inputKey] = normalizedValue;
}
if (wizard.groupAccess) {
const access = wizard.groupAccess;
if (access.helpLines && access.helpLines.length > 0) {
@ -460,6 +720,19 @@ export function buildChannelOnboardingAdapterFromSetupWizard(params: {
});
}
const shouldShowCompletionNote =
wizard.completionNote &&
(wizard.completionNote.shouldShow
? await wizard.completionNote.shouldShow({
cfg: next,
accountId,
credentialValues,
})
: true);
if (shouldShowCompletionNote && wizard.completionNote) {
await prompter.note(wizard.completionNote.lines.join("\n"), wizard.completionNote.title);
}
return { cfg: next, accountId };
},
dmPolicy: wizard.dmPolicy,