---
phase: 06-keyboard-ux-filter-pickers
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - possession/tui/screens/filter_picker.py
autonomous: true
requirements:
  - FILT-01
  - FILT-02
  - FILT-03

must_haves:
  truths:
    - "A single FilterPickerScreen class handles Room, Container, and Category pickers without code duplication"
    - "Picker opens as a floating modal overlay that dims the main screen background"
    - "Type-ahead input at top of picker filters visible list as user types"
    - "j/k keys navigate the list while focus stays on the Input (not ListView)"
    - "Enter confirms selection and calls dismiss(selected_dict); Escape calls dismiss(None)"
    - "Active filter value floats to top with a checkmark marker and shows 'Enter to clear filter' hint"
    - "Empty picker (no entries) shows a dimmed 'No X yet' row; no crashes"
  artifacts:
    - path: "possession/tui/screens/filter_picker.py"
      provides: "FilterPickerScreen(ModalScreen) reusable VIM-style picker"
      exports: ["FilterPickerScreen"]
      min_lines: 80
  key_links:
    - from: "FilterPickerScreen"
      to: "textual.screen.ModalScreen"
      via: "class inheritance"
      pattern: "class FilterPickerScreen\\(ModalScreen\\)"
    - from: "FilterPickerScreen.on_key"
      to: "ListView.action_cursor_down/up"
      via: "programmatic call when j/k pressed"
      pattern: "lv\\.action_cursor_(down|up)"
    - from: "FilterPickerScreen"
      to: "caller MainScreen"
      via: "self.dismiss(result) — dict or None"
      pattern: "self\\.dismiss\\("
---

<objective>
Create `FilterPickerScreen` — a single reusable `ModalScreen` subclass used for all three filter pickers (Room, Container, Category). Parameterized by title, items list, and current active ID.

Purpose: Centralizes all picker UI in one file so Room/Container/Category pickers share identical behavior with zero duplication.
Output: `possession/tui/screens/filter_picker.py` exporting `FilterPickerScreen`.
</objective>

<execution_context>
@/Users/paritoshpatil/.claude/get-shit-done/workflows/execute-plan.md
@/Users/paritoshpatil/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-keyboard-ux-filter-pickers/06-RESEARCH.md
@possession/tui/screens/main.py
@possession/tui/screens/edit.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create FilterPickerScreen modal</name>
  <files>possession/tui/screens/filter_picker.py</files>
  <action>
Create `possession/tui/screens/filter_picker.py` with a single class `FilterPickerScreen(ModalScreen)`.

Constructor signature:
```python
def __init__(self, title: str, items: List[Dict], active_id: Optional[int] = None):
```
- `title`: e.g. "Room" — used in header label and "No Xs yet" message
- `items`: list of dicts with `id` (int) and `name` (str) keys — from `list_rooms()` / `list_containers()` / `list_categories()`
- `active_id`: currently active filter ID or None

Layout (compose):
- Outer `Vertical` container with `id="picker-container"` — centered on screen via CSS `align: center middle`
- `Static(self._title, id="picker-title")` — header label
- `Input(placeholder="Type to filter...", id="picker-search")` — type-ahead search
- `ListView(id="picker-list")` — scrollable item list
- `Static("", id="picker-hint")` — hint text row at bottom

CSS (DEFAULT_CSS on the class):
```
FilterPickerScreen {
    align: center middle;
}
#picker-container {
    width: 52;
    max-height: 16;
    border: solid $primary;
    background: $surface;
    padding: 0 1;
}
#picker-title {
    text-style: bold;
    margin-bottom: 1;
}
#picker-search {
    width: 1fr;
    height: 1;
    border: none;
    background: $surface-darken-1;
    margin-bottom: 1;
}
#picker-list {
    width: 1fr;
    height: auto;
    max-height: 10;
}
#picker-hint {
    height: 1;
    color: $text-muted;
    text-align: center;
}
```

on_mount():
- Call `self._rebuild_list("")` to populate the list
- Call `self.query_one("#picker-search", Input).focus()` — keeps focus on Input always

_rebuild_list(query: str):
- Float active item to top: separate `self._items` into `active_item` (id == `self._active_id`) and `other_items`
- `ordered = ([active_item] if active_item else []) + other_items`
- Filter by query: skip items where `query.lower() not in item["name"].lower()` (skip if query non-empty and no match)
- Store filtered list in `self._filtered: List[Dict]` (needed for Enter to look up selected)
- Clear ListView and re-add: `marker = "✓ " if item["id"] == self._active_id else "  "`, then `lv.append(ListItem(Label(f"{marker}{item['name']}"), id=f"item-{item['id']}"))`
- If `self._items` is empty: add single `ListItem(Label(f"  No {self._title.lower()}s yet"))` — mark as not interactive (do not add to self._filtered)
- If `self._items` non-empty but `self._filtered` empty after filtering: add single `ListItem(Label("  (no matches)"))` — same

on_input_changed(event: Input.Changed):
- Guard `if event.input.id == "picker-search":`
- Call `self._rebuild_list(event.value)`

on_key(event: events.Key):
- `escape`: `self.dismiss(None)`
- `j`: `lv.action_cursor_down(); event.prevent_default()`
- `k`: `lv.action_cursor_up(); event.prevent_default()`
- `enter`: Get `idx = lv.index`; if `idx is not None and idx < len(self._filtered)`: `self.dismiss(self._filtered[idx])`; call `event.prevent_default()`

on_list_view_highlighted(event: ListView.Highlighted):
- Get `hint = self.query_one("#picker-hint", Static)`
- If `event.item` is not None: parse item_id from `event.item.id` (format "item-{id}"); if `item_id == self._active_id`: `hint.update("Enter to clear filter")`; else: `hint.update("")`
- If `event.item` is None: `hint.update("")`

Imports needed: `from textual.screen import ModalScreen`, `from textual.app import ComposeResult`, `from textual.widgets import Input, ListView, ListItem, Label, Static`, `from textual import events`, `from textual.containers import Vertical`, `from typing import Optional, List, Dict`

Do NOT import from `possession` models — this widget is data-agnostic and receives items as a parameter.

Python 3.9 compatibility: use `Optional[int]`, `List[Dict]`, `Optional[str]` — no `int | None` union syntax.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.tui.screens.filter_picker import FilterPickerScreen; print('FilterPickerScreen import OK')"</automated>
    <manual>Confirm no import errors and class is importable</manual>
  </verify>
  <done>
`possession/tui/screens/filter_picker.py` exists, `FilterPickerScreen` imports without error, class inherits from ModalScreen, constructor accepts (title, items, active_id=None).
  </done>
</task>

</tasks>

<verification>
```
cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2
python -c "from possession.tui.screens.filter_picker import FilterPickerScreen; print('OK')"
python -c "import ast; ast.parse(open('possession/tui/screens/filter_picker.py').read()); print('syntax OK')"
```
</verification>

<success_criteria>
- `FilterPickerScreen` importable from `possession.tui.screens.filter_picker`
- Class has `compose()`, `on_mount()`, `_rebuild_list()`, `on_input_changed()`, `on_key()`, `on_list_view_highlighted()` methods
- All Python 3.9 compatible (no union syntax)
- No possession model imports (data-agnostic widget)
</success_criteria>

<output>
After completion, create `.planning/phases/06-keyboard-ux-filter-pickers/06-01-SUMMARY.md`
</output>
