mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Configure: improve searchable model picker token matching
This commit is contained in:
35
src/wizard/clack-prompter.test.ts
Normal file
35
src/wizard/clack-prompter.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { tokenizedOptionFilter } from "./clack-prompter.js";
|
||||
|
||||
describe("tokenizedOptionFilter", () => {
|
||||
it("matches tokens regardless of order", () => {
|
||||
const option = {
|
||||
value: "openai/gpt-5.2",
|
||||
label: "openai/gpt-5.2",
|
||||
hint: "ctx 400k",
|
||||
};
|
||||
|
||||
expect(tokenizedOptionFilter("gpt-5.2 openai/", option)).toBe(true);
|
||||
expect(tokenizedOptionFilter("openai/ gpt-5.2", option)).toBe(true);
|
||||
});
|
||||
|
||||
it("requires all tokens to match", () => {
|
||||
const option = {
|
||||
value: "openai/gpt-5.2",
|
||||
label: "openai/gpt-5.2",
|
||||
};
|
||||
|
||||
expect(tokenizedOptionFilter("gpt-5.2 anthropic/", option)).toBe(false);
|
||||
});
|
||||
|
||||
it("matches against label, hint, and value", () => {
|
||||
const option = {
|
||||
value: "openai/gpt-5.2",
|
||||
label: "GPT 5.2",
|
||||
hint: "provider openai",
|
||||
};
|
||||
|
||||
expect(tokenizedOptionFilter("provider openai", option)).toBe(true);
|
||||
expect(tokenizedOptionFilter("openai gpt-5.2", option)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
text,
|
||||
} from "@clack/prompts";
|
||||
import { createCliProgress } from "../cli/progress.js";
|
||||
import { stripAnsi } from "../terminal/ansi.js";
|
||||
import { note as emitNote } from "../terminal/note.js";
|
||||
import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@@ -26,6 +27,30 @@ function guardCancel<T>(value: T | symbol): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeSearchTokens(search: string): string[] {
|
||||
return search
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
}
|
||||
|
||||
function buildOptionSearchText<T>(option: Option<T>): string {
|
||||
const label = stripAnsi(option.label ?? "");
|
||||
const hint = stripAnsi(option.hint ?? "");
|
||||
const value = String(option.value ?? "");
|
||||
return `${label} ${hint} ${value}`.toLowerCase();
|
||||
}
|
||||
|
||||
export function tokenizedOptionFilter<T>(search: string, option: Option<T>): boolean {
|
||||
const tokens = normalizeSearchTokens(search);
|
||||
if (tokens.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const haystack = buildOptionSearchText(option);
|
||||
return tokens.every((token) => haystack.includes(token));
|
||||
}
|
||||
|
||||
export function createClackPrompter(): WizardPrompter {
|
||||
return {
|
||||
intro: async (title) => {
|
||||
@@ -49,14 +74,26 @@ export function createClackPrompter(): WizardPrompter {
|
||||
}),
|
||||
),
|
||||
multiselect: async (params) => {
|
||||
const prompt = params.searchable ? autocompleteMultiselect : multiselect;
|
||||
const options = params.options.map((opt) => {
|
||||
const base = { value: opt.value, label: opt.label };
|
||||
return opt.hint === undefined ? base : { ...base, hint: stylePromptHint(opt.hint) };
|
||||
}) as Option<(typeof params.options)[number]["value"]>[];
|
||||
|
||||
if (params.searchable) {
|
||||
return guardCancel(
|
||||
await autocompleteMultiselect({
|
||||
message: stylePromptMessage(params.message),
|
||||
options,
|
||||
initialValues: params.initialValues,
|
||||
filter: tokenizedOptionFilter,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return guardCancel(
|
||||
await prompt({
|
||||
await multiselect({
|
||||
message: stylePromptMessage(params.message),
|
||||
options: params.options.map((opt) => {
|
||||
const base = { value: opt.value, label: opt.label };
|
||||
return opt.hint === undefined ? base : { ...base, hint: stylePromptHint(opt.hint) };
|
||||
}) as Option<(typeof params.options)[number]["value"]>[],
|
||||
options,
|
||||
initialValues: params.initialValues,
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user