summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-09-24 19:20:37 +0900
committerAdam Malczewski <[email protected]>2026-09-24 19:20:37 +0900
commit591e2dbe5cbf2283d58debdf92943a075edc4569 (patch)
tree11ee2481ed79f257cd6303f44992471ec9282237 /src/app
parent6556e70e396ec7cc9b2311ef60e00c81722777c7 (diff)
downloaddispatch-web-591e2dbe5cbf2283d58debdf92943a075edc4569.tar.gz
dispatch-web-591e2dbe5cbf2283d58debdf92943a075edc4569.zip
feat(search-backend-selection): Search backend dropdown in Settings
- 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.
Diffstat (limited to 'src/app')
-rw-r--r--src/app/store.svelte.ts68
-rw-r--r--src/app/views/ExtensionsPane.svelte2
-rw-r--r--src/app/views/SettingsPane.svelte5
3 files changed, 75 insertions, 0 deletions
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 {
@@ -531,6 +535,20 @@ export interface AppStore {
*/
concurrencyStatus(): Promise<ConcurrencyStatusResult>;
/**
+ * 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<SearchBackendConfigResult>;
+ /**
+ * 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<SearchBackendConfigResult>;
+ /**
* Fetch the per-slot release cooldown (ms) for one provider
* (`GET /concurrency/cooldown/:providerId`). `404` (no concurrency config at
* all) and `503` (extension not loaded) both surface as `ok: false`.
@@ -2557,6 +2575,56 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
}
},
+ async searchBackendConfig(): Promise<SearchBackendConfigResult> {
+ 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<SearchBackendConfigResult> {
+ 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<ConcurrencyCooldownResult> {
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);
</script>
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 @@
<div class="flex flex-col gap-3">
<ChatLimitField chatLimit={store.chatLimit} save={saveChatLimit} />
+ <SearchBackendField
+ load={() => store.searchBackendConfig()}
+ save={(backend) => store.setSearchBackend(backend)}
+ />
</div>