summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-09-24 19:08:37 +0900
committerAdam Malczewski <[email protected]>2026-09-24 19:08:37 +0900
commit6556e70e396ec7cc9b2311ef60e00c81722777c7 (patch)
tree83d0f7bafb0d620f4b98c64ebad638e2169c8b03
parentcaf7060ec571039102acd7b4fd73008fdecbe8a9 (diff)
downloaddispatch-web-6556e70e396ec7cc9b2311ef60e00c81722777c7.tar.gz
dispatch-web-6556e70e396ec7cc9b2311ef60e00c81722777c7.zip
docs(search-backend-selection): frontend plan (research + planning)
-rw-r--r--notes/search-backend-selection-plan.md112
1 files changed, 112 insertions, 0 deletions
diff --git a/notes/search-backend-selection-plan.md b/notes/search-backend-selection-plan.md
new file mode 100644
index 0000000..b7c9623
--- /dev/null
+++ b/notes/search-backend-selection-plan.md
@@ -0,0 +1,112 @@
+# search-backend-selection — Frontend Plan (research + planning, NOT implemented)
+
+Goal: a "Search backend" dropdown under Settings that lets the user pick the active
+search backend. GLOBAL setting persisted on the backend. Decided: exactly two
+backend entries — "Firecrawl (self-hosted)" and "Umans (Exa)" — plus "None" as the
+off state; save-on-change. Mirrors the backend plan in
+`worktrees/search-backend-selection/backend/notes/`.
+
+## 1. Current state (verified in this worktree)
+
+- `src/app/views/SettingsPane.svelte` is currently FE-local only: a single
+ `ChatLimitField` (chat limit, persisted browser-side). It takes `ports` and calls
+ `store.setChatLimit` — so the pane already has a store seam for persistence.
+- Server-persisted per-workspace settings today follow the heartbeat pattern: own
+ feature dir (`src/features/heartbeat/`) with `logic/view-model.ts` + `ui/*.svelte`,
+ store methods `heartbeatConfig()` / `setHeartbeatConfig()` (store.svelte.ts ~2303),
+ own view pane (`HeartbeatPane.svelte`), registered via `register-views.ts`.
+- `ChatLimitField.svelte` is the canonical settings-field pattern: `$state`
+ value/saving/error/justSaved, `$effect` re-seed while untouched, dirty-guarded
+ save button, Enter-to-save, spinner while saving.
+- Dropdown precedent: `features/chat/ui/ModelSelector.svelte`,
+ `ReasoningEffortSelector.svelte`, `HeartbeatView.svelte` all use native `<select>`
+ with DaisyUI classes.
+- Each FE feature exposes a `manifest` (name/description) listed in
+ `ExtensionsPane.svelte`'s frontend-modules table — a new feature dir adds its
+ manifest there.
+
+## 2. Proposed UI
+
+New feature `src/features/search-settings/`:
+
+```
+features/search-settings/
+├── index.ts # public exports + manifest
+├── logic/view-model.ts # pure: options list, dirty check, validation
+├── logic/view-model.test.ts
+└── ui/SearchBackendField.svelte
+ └── SearchBackendField.test.ts
+```
+
+**`SearchBackendField.svelte`** — modeled on `ChatLimitField`:
+- Native `<select class="select select-bordered select-sm">` bound to local `$state`.
+- Options: "Firecrawl (self-hosted)" and "Umans (Exa)" (disabled with a "not
+ configured" suffix when `!configured` — i.e. no `UMANS_API_KEY`), **plus** a
+ `"none"` option labeled e.g. "None (search disabled)".
+- **Save on change** (decided): a select commits immediately — no separate Set
+ button. Saving/saved/error feedback around the change.
+- States: `saving` (spinner), `saved` ("Saved: <label>."), `error` (inline
+ `text-error`, revert select to last persisted value on failure).
+- Loading state: skeleton/disabled select while `GET` is in flight.
+- Help text below: one line explaining the selection affects the `web_search`
+ tool's *search* mode only — scrape/crawl still route to the self-hosted
+ Firecrawl backend (and `map` is being removed from the tool entirely).
+
+**Placement**: inside `SettingsPane.svelte`, under the Chat limit section (user
+asked for "a dropdown under settings"). The config is GLOBAL, so no
+workspace-switch re-seeding is needed — load once on first mount (or on settings
+view open) and cache in the store; the field still re-seeds from the persisted
+value after a successful save (ChatLimitField's `$effect` pattern).
+
+## 3. Store / API wiring
+
+`store.svelte.ts` (port contract + implementation):
+
+```ts
+searchBackendConfig(): Promise<SearchBackendConfigResult>;
+setSearchBackend(backend: string): Promise<SearchBackendConfigResult>;
+// state: searchBackendConfig | null (loaded), loading flag, last error
+```
+
+Calls the backend contract (see backend plan §2.4 — GLOBAL routes):
+
+```
+GET /search/config → { backend, available: [{id,label,configured}] }
+PUT /search/config { backend } → { backend } | 400 | 503
+```
+(No capabilities field — the selection affects search mode only; scrape/crawl
+always route to Firecrawl. `map` is removed from the tool entirely, per the
+backend plan.)
+
+- Wire types live in `@dispatch/transport-contract` / `wire` (backend plan) — FE
+ types mirror them via the existing wire type imports.
+- Load when the Settings pane mounts (lazy) or on workspace switch; cache like
+ heartbeatConfig does.
+- 503 (extension not loaded) → render the field disabled with "search not
+ available" hint — graceful degradation, matching heartbeat's degrade philosophy.
+
+## 4. Registering the module
+
+- `features/search-settings/index.ts` exports `manifest` (name "Search Settings",
+ description) → add to the `ExtensionsPane.svelte` frontend-modules list so the
+ table doesn't drift from what's composed.
+
+## 5. Tests / verification
+
+- `view-model.test.ts`: options construction (available ∪ none, configured flag →
+ disabled option), dirty check, revert-on-error value computation.
+- `SearchBackendField.test.ts`: render options from config, save-on-change fires
+ `save`, error state reverts selection, loading state disables.
+- Store tests: `searchBackendConfig`/`setSearchBackend` wiring against a fake port,
+ workspace-switch re-seed.
+- Full `bun run typecheck` / `bun run test` / `bun run check` per AGENTS.md.
+
+## 6. Decisions (shared with backend plan §4)
+
+1. Dropdown entries: exactly "Firecrawl (self-hosted)" + "Umans (Exa)", plus
+ "None (search disabled)". Umans sub-provider fixed to `exa`.
+2. GLOBAL setting persisted to the backend (`GET/PUT /search/config`) — no
+ workspace-scoped behavior.
+3. Save-on-change with saved/error feedback (no Set button).
+4. Relay output decided: hits only — the tool returns parsed `SearchHit[]`;
+ the relay model's prose answer is discarded. No open questions remain.