summaryrefslogtreecommitdiffhomepage
path: root/notes/search-backend-selection-plan.md
blob: b7c962306b43151eec1011a8e70030579891ce38 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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.