feat(webui): add BYOK web search settings

Let WebUI users configure the single web search provider credential from BYOK while keeping saved secrets masked and hot-reloaded for new searches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren
2026-05-09 14:52:48 +08:00
committed by Xubin Ren
co-authored by Cursor
parent 7c1aa5ae31
commit 56eee06736
19 changed files with 861 additions and 66 deletions
+15
View File
@@ -7,6 +7,7 @@ import {
listSlashCommands,
updateProviderSettings,
updateSettings,
updateWebSearchSettings,
} from "@/lib/api";
describe("webui API helpers", () => {
@@ -71,6 +72,20 @@ describe("webui API helpers", () => {
);
});
it("serializes web search settings updates", async () => {
await updateWebSearchSettings("tok", {
provider: "searxng",
baseUrl: "https://search.example.com",
});
expect(fetch).toHaveBeenCalledWith(
"/api/settings/web-search/update?provider=searxng&base_url=https%3A%2F%2Fsearch.example.com",
expect.objectContaining({
headers: { Authorization: "Bearer tok" },
}),
);
});
it("maps generated session titles from the sessions list", async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
+51 -1
View File
@@ -181,7 +181,12 @@ describe("App layout", () => {
has_api_key: true,
},
providers: [
{ name: "openai", label: "OpenAI", configured: true },
{
name: "openai",
label: "OpenAI",
configured: true,
api_key_hint: "open••••-key",
},
{
name: "openrouter",
label: "OpenRouter",
@@ -189,6 +194,16 @@ describe("App layout", () => {
default_api_base: "https://openrouter.ai/api/v1",
},
],
web_search: {
provider: "brave",
api_key_hint: "BSAo••••ew20",
base_url: null,
providers: [
{ name: "duckduckgo", label: "DuckDuckGo", credential: "none" },
{ name: "brave", label: "Brave Search", credential: "api_key" },
{ name: "tavily", label: "Tavily", credential: "api_key" },
],
},
runtime: {
config_path: "/tmp/config.json",
},
@@ -219,8 +234,34 @@ describe("App layout", () => {
expect(screen.getByText("AI")).toBeInTheDocument();
expect(screen.getByDisplayValue("openai/gpt-4o")).toBeInTheDocument();
fireEvent.click(within(settingsNav).getByRole("button", { name: "BYOK" }));
expect(screen.getByRole("tab", { name: "LLM" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tab", { name: "Web Search" })).toBeInTheDocument();
expect(screen.getByText("OpenRouter")).toBeInTheDocument();
expect(screen.getAllByText("Not configured").length).toBeGreaterThan(0);
fireEvent.click(screen.getByText("OpenAI"));
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
fireEvent.change(screen.getByPlaceholderText("Leave blank to keep the current key"), {
target: { value: "unsaved-openai-key" },
});
fireEvent.click(screen.getByText("OpenRouter"));
fireEvent.click(screen.getByText("OpenAI"));
expect(screen.getByText("open••••-key")).toBeInTheDocument();
expect(screen.queryByDisplayValue("unsaved-openai-key")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("tab", { name: "Web Search" }));
expect(screen.getByText("Search provider")).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Brave Search/ })).toBeInTheDocument();
expect(screen.getByText("BSAo••••ew20")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
fireEvent.change(screen.getByPlaceholderText("Leave blank to keep the current key"), {
target: { value: "unsaved-brave-key" },
});
fireEvent.pointerDown(screen.getByRole("button", { name: /Brave Search/ }));
fireEvent.click(screen.getByRole("menuitem", { name: "Tavily" }));
fireEvent.pointerDown(screen.getByRole("button", { name: /Tavily/ }));
fireEvent.click(screen.getByRole("menuitem", { name: "Brave Search" }));
expect(screen.getByText("BSAo••••ew20")).toBeInTheDocument();
expect(screen.queryByDisplayValue("unsaved-brave-key")).not.toBeInTheDocument();
});
it("returns from settings to an available chat instead of the blank start page", async () => {
@@ -257,6 +298,15 @@ describe("App layout", () => {
has_api_key: true,
},
providers: [{ name: "openai", label: "OpenAI", configured: true }],
web_search: {
provider: "duckduckgo",
api_key_hint: null,
base_url: null,
providers: [
{ name: "duckduckgo", label: "DuckDuckGo", credential: "none" },
{ name: "brave", label: "Brave Search", credential: "api_key" },
],
},
runtime: {
config_path: "/tmp/config.json",
},