# Ticket 11.16 — Model Filter Settings

**Phase**: 11 — UI/UX Polish
**Priority**: Medium
**Depends on**: 5.2

## Description

Users with many providers configured face a long, unwieldy model list in the `ModelSelector` dropdown. This ticket adds a **Model Filter** settings flow that lets users choose which models appear in that dropdown, with separate selections for global preferences and per-project overrides.

The flow begins from the existing settings button (`sliders-horizontal` icon) in the header, which today opens the `NotifSettings` dropdown. That dropdown gains a new **"Model filter…"** link/button that opens a dedicated `ModelFilterModal`. The modal has two tabs — **Global** and **Project** — each showing the full list of available models as a multi-select checklist. Checked models are included in the dropdown; unchecked models are hidden. A **Save** button persists the selection to config, applies it immediately to the live dropdown, and closes the modal.

Config is stored in two places:
- **Global**: `localStorage["model-filter-global"]` — applies everywhere no project config overrides it
- **Project**: `localStorage["model-filter:<slug>"]` — applies for the current project/repo slug, overrides global

Config is read at:
1. App startup (initial load of `ModelSelector`)
2. Project switch (when the project slug changes in `ChatLayout.svelte`)
3. Immediately after saving from the modal

## Acceptance Criteria

### AC1: Settings button opens Model Filter entry point

```gherkin
Given the relay UI is loaded
When the user taps/clicks the sliders-horizontal settings icon in the header
Then the NotifSettings dropdown opens as today
And it contains a "Model filter…" button/link below the existing notification toggles
```

### AC2: Model Filter Modal opens with two tabs

```gherkin
Given the NotifSettings dropdown is open
When the user clicks "Model filter…"
Then the NotifSettings dropdown closes
And a modal opens titled "Model Filter"
And the modal has two tabs: "Global" and "Project"
And the "Global" tab is active by default
```

### AC3: Each tab shows the full model list as a multi-select checklist

```gherkin
Given the Model Filter modal is open on the "Global" tab
Then every model currently available in ModelSelector is listed
And each model shows its name and provider (matching ModelSelector display format)
And each model has a checkbox (checked = visible in dropdown, unchecked = hidden)
And all models are checked by default when no filter config exists yet
```

### AC4: Project tab reflects per-project state

```gherkin
Given the Model Filter modal is open and the user switches to the "Project" tab
Then the same full model list is shown
And checked/unchecked state reflects the saved project-level filter for the current slug
And if no project filter is saved, all models are checked by default
And the tab label shows the current project slug so the user knows which project is affected
```

### AC5: Save persists config and applies immediately

```gherkin
Given the user has unchecked some models in the "Global" tab
And optionally checked/unchecked models in the "Project" tab
When the user clicks "Save"
Then the modal closes
And the global selection is written to localStorage["model-filter-global"]
And the project selection is written to localStorage["model-filter:<slug>"]
And ModelSelector immediately reflects the new filter without a page reload
```

### AC6: ModelSelector respects active filter

```gherkin
Given a model filter is saved for the current project
When ModelSelector renders its list
Then only models present in the project filter are shown
And if no project filter exists, the global filter is applied instead
And if neither filter exists, all models are shown (no filtering)
```

### AC7: Filter loaded on startup

```gherkin
Given the user has saved a global model filter on a previous visit
When the app loads fresh
Then ModelSelector only shows models matching the saved global filter
And if a project filter also exists for the current slug, it takes precedence
```

### AC8: Filter reloaded on project switch

```gherkin
Given the user switches from project "foo" (which has a project filter) to project "bar" (which has none)
When the project switch completes in ChatLayout.svelte
Then ModelSelector shows the global filter models for project "bar"
And switching back to "foo" restores "foo"'s project filter
```

### AC9: Empty filter handled gracefully

```gherkin
Given the user unchecks all models in both tabs and saves
When ModelSelector renders
Then it shows all models (fall back to unfiltered) rather than an empty list
And the next time the modal opens, all models are shown as checked
```

## Files to Create

- `src/lib/public/components/overlays/ModelFilterModal.svelte` — the modal with two tabs and checklist
- `src/lib/public/utils/model-filter.ts` — `getModelFilter(slug?)`, `saveModelFilter(global, project, slug)`, `applyModelFilter(models, slug?)` helpers

## Files to Modify

- `src/lib/public/components/overlays/NotifSettings.svelte` — add "Model filter…" button that emits an event or calls a callback to open the modal
- `src/lib/public/components/layout/Header.svelte` — handle the open-modal event from NotifSettings, render `<ModelFilterModal>`
- `src/lib/public/components/features/ModelSelector.svelte` — call `applyModelFilter()` when building the displayed model list; re-run on project slug change
- `src/lib/public/components/layout/ChatLayout.svelte` — trigger model filter reload in the project-switch `$effect` (alongside existing `clearDiscoveryState()` calls)

## Testable Components

| Component | Test Type | What to Assert |
|-----------|-----------|----------------|
| `getModelFilter` / `saveModelFilter` | Unit | Reads/writes correct `localStorage` keys; returns `null` when not set |
| `applyModelFilter` | Unit | Returns full list when no filter; filters correctly; falls back to global when no project filter; returns full list when filter is empty |
| Model Filter Modal rendering | E2E | Opens from settings icon; both tabs show full model list with checkboxes |
| Save persists and closes | E2E | After save, `localStorage` keys are set; modal is dismissed |
| ModelSelector filtered | E2E | After saving a filter, only selected models appear in the dropdown |
| Project-switch reload | E2E | Switching projects updates the visible models according to project-specific config |
| Startup load | E2E | Reloading the page applies the previously saved filter without opening the modal |

## Reference

- Header settings button: `src/lib/public/components/layout/Header.svelte` (lines ~149–158)
- NotifSettings (existing dropdown): `src/lib/public/components/overlays/NotifSettings.svelte`
- Notification settings persistence pattern: `src/lib/public/utils/notif-settings.ts`
- ModelSelector (dropdown to filter): `src/lib/public/components/features/ModelSelector.svelte`
- Discovery state (model list source): `src/lib/public/stores/discovery.svelte.ts`
- Project switch hook: `src/lib/public/components/layout/ChatLayout.svelte` (lines ~135–171)
- Project slug store: `src/lib/public/stores/project.svelte.ts`
