# Spec: Context-scoped search with filter chips and configurable group order

> ⚠️ Superseded in part by scoped-search-boundary.md. The "frontend only, no Rust changes" decision below (§Background 2, §Design "Scope model" and "Threading scope through the store") left Jellyfin's item-type taxonomy in the presentation layer, which violates the backend/frontend boundary. The taxonomy is being moved into Rust. The user-facing behaviour and UX in this spec are unchanged; only where the scope→item-type mapping and result bucketing live changes. Read the boundary spec before touching search code.

> Progress: the scope→item-type mapping now lives in Rust (`SearchScope::item_types()`); the frontend sends an opaque scope. Result-side bucketing (`GROUP_ITEM_TYPES`) is still frontend-side — see scoped-search-boundary-implementation.md §Stage 2.

Status: Implemented (boundary revision: query side done, result side pending) Scope: Frontend only. No Rust changes required. (Revised — see banner.) Requirements: UR-049 → DR-063, DR-064, DR-065; UR-050 → DR-066, DR-067 (see requirements.md). UX spec: ux-flows.md §6 — §6.1 scope, §6.2 layout, §6.3 group order, §6.4 current deviations.

## Summary

Two related changes to search:

1. Scope — a search started inside a library searches that library. Started from Home, `/library`, or the search tab, it searches everything. The active scope shows as a chip row under the search bar, preselected from context and freely changeable without retyping.
2. Group order — the order result groups appear in (Songs, Albums, Artists, Movies, TV Shows) becomes a drag-and-drop setting instead of being hardcoded.

## Motivation

Searching "office" while browsing TV currently returns music albums, because both search entry points call the same unscoped query. The user has already told us what they're looking at; ignoring that makes search feel indiscriminate and pushes the relevant result below unrelated media.

## Background: what already exists

Verified in code — most of the plumbing is already there. This is substantially a wiring task, not new infrastructure.

1. `SearchOptions` already carries the filter. bindings.ts — `SearchOptions = { limit?, includeItemTypes?, searchTerm? }`.

2. Rust already honours `include_item_types` on both paths — online (online.rs, in the `get_items` options mapping) and offline (offline.rs, which builds a SQL type filter from it). Do not add Rust code for filtering.

3. Per-page list search already does this correctly. GenericMediaListPage.svelte passes `includeItemTypes: [config.itemType]` to `repo.search(...)`. Use it as the reference for the call shape, including the `requestId` handling.

4. The gap is exactly one function. library.ts — `search(query)` takes only a query and calls `repo.search(query, { limit: 10000 }, requestId)`, dropping any scope. Both callers (search/+page.svelte and library/+layout.svelte) go through it.

5. Group order is hardcoded in markup. SearchResults.svelte categorizes into `music{tracks,albums,artists} / movies / tvShows` and renders three fixed sections in source order.

6. Frontend preferences persist via `localStorage`, per the existing `viewMode` precedent in library.ts (`jellytau-view-mode`). Follow that pattern — do not add a Rust settings command for this.

## Design

### Scope model

One `SearchScope` type, defined once and shared:

Scope | `includeItemTypes` | Chip label
--- | --- | ---
`all` | unset | All
`music` | `MusicAlbum`, `MusicArtist`, `Audio`, `Playlist` | Music
`movies` | `Movie` | Movies
`tv` | `Series`, `Episode` | TV

`all` must send no `includeItemTypes` key rather than a list of every type — the two are not equivalent for item types not enumerated here (Person, folders).

### Route → scope resolution (DR-063)

A pure function, unit-testable without a DOM:

    resolveSearchScope(pathname: string): SearchScope

- `/library/music*` → `music`
- `/library/movies*` → `movies`
- `/library/tv*` → `tv`
- `/`, `/library`, `/search`, anything else → `all`

Note `/library/shows/genres` exists as a route; treat `shows` as `tv`. Check the current route list before finalising — do not assume this table is exhaustive.

### Scope is a starting point, not a lock (DR-064)

The resolved scope sets the initial chip only. Once the user taps a chip, their choice governs until they leave the search surface. Concretely: derive the initial value from the route, hold it in component state, and do not re-derive it on every navigation — otherwise a user who widens to All snaps back to TV.

Changing a chip re-runs the current query at the new scope. Changing the query keeps the current scope.

### Threading scope through the store (DR-065)

Extend the store's search signature to accept an optional scope and pass `includeItemTypes` down to `repo.search`. Preserve the existing behaviour exactly: the `requestId` bump, the stale-response guard, the `search-event` listener merge, the 10s timeout, and the empty-query clear path. This is an additive parameter — no caller should break.

### Group order (DR-066, DR-067)

Persist an ordered array of group ids:

    ["songs", "albums", "artists", "movies", "tvShows"]   // shipped default

Rendering composes scope and order as two independent axes, in this order:

1. drop groups outside the active scope,
2. sort the remainder by the user's saved order,
3. omit groups that came back empty.

Scope never rewrites the saved order — narrowing to Music and back to All must restore the user's full arrangement. See ux-flows.md §6.3 for the worked example.

Settings gets a reorderable list. Dragging alone is not sufficient: provide keyboard-operable move up/down controls with proper labels, or the setting is unusable with a screen reader and on any pointerless input.

Unknown or missing ids in the stored array must not crash rendering — treat the stored order as a hint, append any group it doesn't mention, and ignore ids that no longer exist. A user upgrading from a build with fewer groups must not lose the new ones.

## Out of scope

- Ranking within a group. Order is presentation-only.
- Server-side search ranking or the Jellyfin query itself.
- Scope chips on the per-page list search in `GenericMediaListPage` — that page is already implicitly scoped by its own `itemType`.
- Any Rust change.

## Acceptance criteria

- Searching from inside Music returns no movies or TV; from inside TV, no music.
- Searching from Home, `/library`, or the search tab returns all types.
- The chip row renders under the search bar on both the search page and the in-library header search, with the context-derived chip preselected.
- Tapping a chip re-runs the search with the query preserved; editing the query preserves the selected chip.
- Tapping "All" from a context-scoped search widens results without retyping.
- Result groups render in the user's configured order, with out-of-scope and empty groups omitted and relative order preserved.
- Group order is reorderable by drag and by keyboard, persists across restarts, and ships with the documented default.
- Offline search respects scope (the offline path already filters — verify, don't reimplement).
- `bun run check` and `bun run test` pass.

## Testing

Follow the existing frontend test conventions (vitest, `src/lib/**/*.test.ts`).

- `resolveSearchScope` — pure unit tests over the route table, including the `/library/shows/genres` case and unknown routes falling back to `all`.
- Scope → `includeItemTypes` mapping, asserting `all` omits the key entirely.
- The compose step: scope filter + user order + empty-group omission, including the "narrow then widen restores order" case and a stored order containing an unknown id.
- Store-level: scoped search forwards `includeItemTypes` to the repository, and the existing stale-`requestId` guard still discards superseded responses.

New requirement-implementing code needs `TRACES:` comments — see CLAUDE.md. Suggested tags: the scope resolver and chip row `UR-049 | DR-063, DR-064`, the store change `UR-049 | DR-065`, the settings list and ordered rendering `UR-050 | DR-066, DR-067`.

## Notes for the implementer

- Read ux-flows.md §6 first — it is the behavioural spec; this document is the implementation plan.
- The IPC camelCase rule applies to anything new that crosses the boundary (CLAUDE.md) — though this change should not add commands.
- Another session may be active in this repo. Check `git diff` before "repairing" unexpected changes.