fix: split search provider configure and switch flows

This commit is contained in:
Tak Hoffman 2026-03-15 12:17:49 -05:00
parent f4ea5221df
commit 98c5c04608
3 changed files with 46 additions and 25 deletions

View File

@ -237,7 +237,7 @@ describe("runConfigureWizard", () => {
mocks.clackOutro.mockResolvedValue(undefined); mocks.clackOutro.mockResolvedValue(undefined);
mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true); mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true);
mocks.clackSelect.mockImplementation(async (params: { message: string }) => { mocks.clackSelect.mockImplementation(async (params: { message: string }) => {
if (params.message === "Choose web search provider") { if (params.message === "Choose active web search provider") {
return "tavily"; return "tavily";
} }
if (params.message.startsWith("Search depth")) { if (params.message.startsWith("Search depth")) {
@ -317,7 +317,7 @@ describe("runConfigureWizard", () => {
mocks.clackOutro.mockResolvedValue(undefined); mocks.clackOutro.mockResolvedValue(undefined);
mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(false); mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(false);
mocks.clackSelect.mockImplementation(async (params: { message: string }) => { mocks.clackSelect.mockImplementation(async (params: { message: string }) => {
if (params.message === "Choose web search provider") { if (params.message === "Choose active web search provider") {
return "brave"; return "brave";
} }
return "__continue"; return "__continue";
@ -411,7 +411,7 @@ describe("runConfigureWizard", () => {
mocks.clackOutro.mockResolvedValue(undefined); mocks.clackOutro.mockResolvedValue(undefined);
mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true); mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true);
mocks.clackSelect.mockImplementation(async (params: { message: string }) => { mocks.clackSelect.mockImplementation(async (params: { message: string }) => {
if (params.message === "Choose web search provider") { if (params.message === "Choose active web search provider") {
return "tavily"; return "tavily";
} }
if (params.message.startsWith("Search depth")) { if (params.message.startsWith("Search depth")) {
@ -571,7 +571,7 @@ describe("runConfigureWizard", () => {
mocks.clackOutro.mockResolvedValue(undefined); mocks.clackOutro.mockResolvedValue(undefined);
mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true); mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true);
mocks.clackSelect.mockImplementation(async (params: { message: string }) => { mocks.clackSelect.mockImplementation(async (params: { message: string }) => {
if (params.message === "Choose web search provider") { if (params.message === "Choose active web search provider") {
return "tavily"; return "tavily";
} }
if (params.message.startsWith("Search depth")) { if (params.message.startsWith("Search depth")) {
@ -784,7 +784,7 @@ describe("runConfigureWizard", () => {
mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true); mocks.clackConfirm.mockResolvedValueOnce(true).mockResolvedValueOnce(true);
mocks.clackSelect.mockImplementation( mocks.clackSelect.mockImplementation(
async (params: { message: string; options?: Array<{ value: string; hint?: string }> }) => { async (params: { message: string; options?: Array<{ value: string; hint?: string }> }) => {
if (params.message === "Choose web search provider") { if (params.message === "Choose active web search provider") {
expect(params.options?.[0]).toMatchObject({ expect(params.options?.[0]).toMatchObject({
value: "tavily", value: "tavily",
hint: "Plugin search · External plugin", hint: "Plugin search · External plugin",

View File

@ -166,7 +166,7 @@ describe("setupSearch", () => {
await setupSearch(cfg, runtime, prompter); await setupSearch(cfg, runtime, prompter);
const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find( const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
(call) => call[0]?.message === "Choose web search provider", (call) => call[0]?.message === "Choose active web search provider",
); );
expect(providerSelectCall?.[0]).toEqual( expect(providerSelectCall?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -225,7 +225,7 @@ describe("setupSearch", () => {
await setupSearch(cfg, runtime, prompter); await setupSearch(cfg, runtime, prompter);
const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find( const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
(call) => call[0]?.message === "Choose web search provider", (call) => call[0]?.message === "Choose active web search provider",
); );
expect(providerSelectCall?.[0]).toEqual( expect(providerSelectCall?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -294,7 +294,7 @@ describe("setupSearch", () => {
await setupSearch(cfg, runtime, prompter); await setupSearch(cfg, runtime, prompter);
const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find( const providerSelectCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
(call) => call[0]?.message === "Choose web search provider", (call) => call[0]?.message === "Choose active web search provider",
); );
const matchingOptions = const matchingOptions =
providerSelectCall?.[0]?.options?.filter( providerSelectCall?.[0]?.options?.filter(
@ -359,7 +359,7 @@ describe("setupSearch", () => {
expect(prompter.select).toHaveBeenCalledWith( expect(prompter.select).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: "Choose web search provider", message: "Choose active web search provider",
options: expect.arrayContaining([ options: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
value: "__install_plugin__", value: "__install_plugin__",
@ -462,7 +462,7 @@ describe("setupSearch", () => {
await setupSearch(cfg, runtime, prompter); await setupSearch(cfg, runtime, prompter);
const options = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find( const options = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
(call) => call[0]?.message === "Choose web search provider", (call) => call[0]?.message === "Choose active web search provider",
)?.[0]?.options; )?.[0]?.options;
expect(options[0]).toMatchObject({ expect(options[0]).toMatchObject({
value: "tavily", value: "tavily",
@ -528,7 +528,7 @@ describe("setupSearch", () => {
await setupSearch(cfg, runtime, prompter); await setupSearch(cfg, runtime, prompter);
const configurePickerCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find( const configurePickerCall = (prompter.select as ReturnType<typeof vi.fn>).mock.calls.find(
(call) => call[0]?.message === "Choose web search provider", (call) => call[0]?.message === "Choose active web search provider",
); );
expect(configurePickerCall?.[0]).toEqual( expect(configurePickerCall?.[0]).toEqual(
expect.objectContaining({ expect.objectContaining({

View File

@ -30,6 +30,7 @@ import {
} from "./onboarding/plugin-install.js"; } from "./onboarding/plugin-install.js";
import { import {
buildProviderSelectionOptions, buildProviderSelectionOptions,
promptProviderManagementIntent,
type ProviderManagementIntent, type ProviderManagementIntent,
} from "./provider-management.js"; } from "./provider-management.js";
import { import {
@ -42,6 +43,8 @@ export type SearchProvider = string;
const SEARCH_PROVIDER_INSTALL_SENTINEL = "__install_plugin__" as const; const SEARCH_PROVIDER_INSTALL_SENTINEL = "__install_plugin__" as const;
const SEARCH_PROVIDER_KEEP_CURRENT_SENTINEL = "__keep_current__" as const; const SEARCH_PROVIDER_KEEP_CURRENT_SENTINEL = "__keep_current__" as const;
const SEARCH_PROVIDER_SKIP_SENTINEL = "__skip__" as const; const SEARCH_PROVIDER_SKIP_SENTINEL = "__skip__" as const;
const SEARCH_PROVIDER_CONFIGURE_SENTINEL = "__configure_provider__" as const;
const SEARCH_PROVIDER_SWITCH_ACTIVE_SENTINEL = "__switch_active_provider__" as const;
type PluginSearchProviderEntry = { type PluginSearchProviderEntry = {
kind: "plugin"; kind: "plugin";
@ -1231,14 +1234,43 @@ export async function promptSearchProviderFlow(params: {
includeSkipOption: params.includeSkipOption, includeSkipOption: params.includeSkipOption,
skipHint: params.skipHint, skipHint: params.skipHint,
}); });
const action = await promptProviderManagementIntent({
prompter: params.prompter,
message: "Web search setup",
includeSkipOption: params.includeSkipOption,
configuredCount: pickerModel.configuredCount,
configureValue: SEARCH_PROVIDER_CONFIGURE_SENTINEL,
switchValue: SEARCH_PROVIDER_SWITCH_ACTIVE_SENTINEL,
skipValue: SEARCH_PROVIDER_SKIP_SENTINEL,
configureLabel: "Configure or install a provider",
configureHint:
"Update keys, plugin settings, or install a provider without changing the active provider",
switchLabel: "Switch active provider",
switchHint: "Change which provider web_search uses right now",
skipHint: params.skipHint ?? "Configure later with openclaw configure --section web",
});
if (action === SEARCH_PROVIDER_SKIP_SENTINEL) {
return params.config;
}
const intent: SearchProviderFlowIntent =
action === SEARCH_PROVIDER_CONFIGURE_SENTINEL ? "configure-provider" : "switch-active";
const choice = await params.prompter.select<SearchProviderPickerChoice>({ const choice = await params.prompter.select<SearchProviderPickerChoice>({
message: "Choose web search provider", message:
intent === "switch-active"
? "Choose active web search provider"
: "Choose provider to configure",
options: buildProviderSelectionOptions({ options: buildProviderSelectionOptions({
intent: "configure-provider", intent,
options: pickerModel.options, options: pickerModel.options,
activeValue: pickerModel.activeProvider, activeValue: pickerModel.activeProvider,
hiddenValues: intent === "configure-provider" ? [SEARCH_PROVIDER_KEEP_CURRENT_SENTINEL] : [],
}), }),
initialValue: pickerModel.initialValue, initialValue:
intent === "switch-active"
? pickerModel.initialValue
: (pickerModel.options.find(
(option) => option.value !== SEARCH_PROVIDER_KEEP_CURRENT_SENTINEL,
)?.value ?? pickerModel.initialValue),
}); });
if ( if (
@ -1247,17 +1279,6 @@ export async function promptSearchProviderFlow(params: {
) { ) {
return params.config; return params.config;
} }
const selectedEntry = providerEntries.find((entry) => entry.value === choice);
const intent: SearchProviderFlowIntent =
choice === SEARCH_PROVIDER_INSTALL_SENTINEL
? "configure-provider"
: choice === pickerModel.activeProvider
? "configure-provider"
: selectedEntry?.configured
? "switch-active"
: "configure-provider";
return applySearchProviderChoice({ return applySearchProviderChoice({
config: params.config, config: params.config,
choice, choice,