From 591e2dbe5cbf2283d58debdf92943a075edc4569 Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 24 Sep 2026 19:20:37 +0900 Subject: feat(search-backend-selection): Search backend dropdown in Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New features/search-settings: pure view-model (options = available backends ∪ 'None (search disabled)', unconfigured entries disabled with a suffix, unknown persisted selection prepended, network-seam normalization) + the SearchBackendField dropdown (save-on-change, spinner/saved/error states, revert-to-persisted on failure, 503 -> disabled field with a 'search not available' hint, loading skeleton, help text noting search-mode-only scope). - store.svelte.ts: searchBackendConfig() / setSearchBackend() over the global GET/PUT /search/config routes (503 marked unavailable on the failure variant). - SettingsPane renders the field under the chat limit; ExtensionsPane lists the new module manifest. - .dispatch mirrors: wire + transport-contract deltas for the new types/routes. --- src/app/store.svelte.ts | 68 +++++++++++++++++++++++++++++++++++++ src/app/views/ExtensionsPane.svelte | 2 ++ src/app/views/SettingsPane.svelte | 5 +++ 3 files changed, 75 insertions(+) (limited to 'src/app') diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index a858927..73b19bc 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -102,6 +102,10 @@ import type { import { normalizeNamedAgentRecord, normalizeNamedAgents } from "../features/named-agents"; import type { PluginsStore } from "../features/plugins"; import { createPluginsHttp, createPluginsStore } from "../features/plugins"; +import { + normalizeSearchBackendConfig, + type SearchBackendConfigResult, +} from "../features/search-settings"; import type { Tab, TabsState } from "../features/tabs"; import { createTabsStore, deriveTitle, type TabsStore } from "../features/tabs"; import { @@ -530,6 +534,20 @@ export interface AppStore { * an empty list when the extension isn't loaded (`{ providers: [] }`). */ concurrencyStatus(): Promise; + /** + * Load the GLOBAL search-backend selection + * (`GET /search/config`): which engine backs the `web_search` tool's search + * mode. Not workspace-scoped — the selection is global. `unavailable: true` + * on the failure variant marks the 503 degrade (the backend's + * search-config service not loaded) so the UI can disable the field. + */ + searchBackendConfig(): Promise; + /** + * Persist a new search-backend selection + * (`PUT /search/config`, body `{ backend }`). `400` (unknown/unconfigured + * backend) and `503` (service not loaded) both surface as `ok: false`. + */ + setSearchBackend(backend: string): Promise; /** * Fetch the per-slot release cooldown (ms) for one provider * (`GET /concurrency/cooldown/:providerId`). `404` (no concurrency config at @@ -2557,6 +2575,56 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } }, + async searchBackendConfig(): Promise { + try { + const res = await fetchImpl(`${httpBase}/search/config`); + if (!res.ok) { + const errBody = (await res.json().catch(() => null)) as { error?: string } | null; + return { + ok: false, + error: errBody?.error ?? `Search config failed (HTTP ${res.status})`, + // The 503 degrade = the backend's search-config service not loaded + // (the tool-web-search extension absent) — a distinct UI state. + ...(res.status === 503 ? { unavailable: true } : {}), + }; + } + // Normalize the untyped JSON at this network seam (pure helper) so a + // malformed/partial response can never crash the renderer. + const config = normalizeSearchBackendConfig(await res.json()); + return { ok: true, config }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Search config request failed", + }; + } + }, + + async setSearchBackend(backend: string): Promise { + try { + const res = await fetchImpl(`${httpBase}/search/config`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ backend }), + }); + if (!res.ok) { + const errBody = (await res.json().catch(() => null)) as { error?: string } | null; + return { + ok: false, + error: errBody?.error ?? `Set search backend failed (HTTP ${res.status})`, + ...(res.status === 503 ? { unavailable: true } : {}), + }; + } + const config = normalizeSearchBackendConfig(await res.json()); + return { ok: true, config }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Set search backend request failed", + }; + } + }, + async getConcurrencyCooldown(providerId: string): Promise { try { const res = await fetchImpl( diff --git a/src/app/views/ExtensionsPane.svelte b/src/app/views/ExtensionsPane.svelte index 7a2343e..6afc2bf 100644 --- a/src/app/views/ExtensionsPane.svelte +++ b/src/app/views/ExtensionsPane.svelte @@ -17,6 +17,7 @@ import { manifest as markdownManifest } from "../../features/markdown"; import { manifest as mcpManifest } from "../../features/mcp"; import { manifest as namedAgentsManifest } from "../../features/named-agents"; + import { manifest as searchSettingsManifest } from "../../features/search-settings"; import { manifest as settingsManifest } from "../../features/settings"; import { manifest as smartScrollManifest } from "../../features/smart-scroll"; import { SurfaceView } from "../../features/surface-host"; @@ -60,6 +61,7 @@ concurrencyManifest, visionManifest, namedAgentsManifest, + searchSettingsManifest, ].map((m) => [m.name, m.description] as const); diff --git a/src/app/views/SettingsPane.svelte b/src/app/views/SettingsPane.svelte index 67a88ec..f119103 100644 --- a/src/app/views/SettingsPane.svelte +++ b/src/app/views/SettingsPane.svelte @@ -8,6 +8,7 @@ */ import { tick } from "svelte"; import { ChatLimitField, type ChatLimitSaveResult } from "../../features/settings"; + import { SearchBackendField } from "../../features/search-settings"; import type { AppViewPorts } from "../view-ports"; let { ports }: { ports: AppViewPorts } = $props(); @@ -31,4 +32,8 @@
+ store.searchBackendConfig()} + save={(backend) => store.setSearchBackend(backend)} + />
-- cgit v1.2.3