---
phase: 06-keyboard-ux-filter-pickers
plan: "02"
type: execute
wave: 2
depends_on:
  - "06-01"
files_modified:
  - possession/tui/screens/main.py
  - possession/tui/widgets/statsbar.py
autonomous: true
requirements:
  - FILT-01
  - FILT-02
  - FILT-03

must_haves:
  truths:
    - "Pressing r opens a Room picker; selecting a room narrows the item list; Escape cancels without change"
    - "Pressing c opens a Container picker; pressing t opens a Category picker — identical behavior"
    - "Selecting an already-active filter value clears the filter (toggle off)"
    - "Stats bar item count shows the filtered count (len of current items), not the global total"
    - "Active filters appear in the stats bar inline: '12 items [Room: Kitchen] [Category: Audio]'"
    - "Zero-match state shows 'No items match the current filters' row in DataTable; stats bar still shows filter tags"
    - "Filter names remain visible in stats bar even when zero items match (names cached at pick time)"
  artifacts:
    - path: "possession/tui/screens/main.py"
      provides: "r/c/t bindings, filter state, picker callbacks, _load_items with filter params"
      contains: "_filter_room_id"
    - path: "possession/tui/widgets/statsbar.py"
      provides: "refresh_stats() accepting item_count_override and filter_tags string"
      contains: "filter_tags"
  key_links:
    - from: "MainScreen.action_open_room_picker"
      to: "FilterPickerScreen"
      via: "self.app.push_screen(FilterPickerScreen('Room', rooms, self._filter_room_id), self._on_room_picked)"
      pattern: "push_screen.*FilterPickerScreen"
    - from: "MainScreen._on_room_picked"
      to: "MainScreen._load_items"
      via: "called after filter state updated"
      pattern: "_load_items\\(\\)"
    - from: "MainScreen._load_items"
      to: "list_items"
      via: "called with room_id, container_id, category_id filter args"
      pattern: "room_id=self\\._filter_room_id"
    - from: "MainScreen._refresh_stats"
      to: "StatsBar.refresh_stats"
      via: "passes len(self._items) as item_count_override and filter tag string"
      pattern: "refresh_stats.*item_count_override"
---

<objective>
Wire `r`/`c`/`t` keyboard bindings into `MainScreen` to open `FilterPickerScreen` for Room, Container, and Category filters. Store filter state (`_filter_room_id`, `_filter_container_id`, `_filter_category_id` and their name counterparts). Update `_load_items()` to pass filter IDs to `list_items()`. Update `StatsBar.refresh_stats()` to accept filtered item count and filter tag strings.

Purpose: Connects the picker widget to the live item list, making filters work end-to-end.
Output: Working `r`/`c`/`t` pickers with stats bar filter tags and zero-match empty state.
</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
@.planning/phases/06-keyboard-ux-filter-pickers/06-01-SUMMARY.md
@possession/tui/screens/main.py
@possession/tui/widgets/statsbar.py
@possession/models.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update StatsBar to accept filtered count and filter tags</name>
  <files>possession/tui/widgets/statsbar.py</files>
  <action>
Modify `StatsBar.refresh_stats()` to accept two new optional parameters:
- `item_count_override: Optional[int] = None` — when provided, use this value instead of `stats["item_count"]` for the items stat display
- `filter_tags: str = ""` — when non-empty, appended to the item count with a space

Updated signature:
```python
def refresh_stats(self, db_path, item_count_override: Optional[int] = None, filter_tags: str = "") -> None:
```

Inside the method:
- Call `get_stats(db_path)` as before for rooms, containers, total_value
- For the items count display: `display_count = item_count_override if item_count_override is not None else stats["item_count"]`
- Build item text: `item_text = str(display_count)` then `if filter_tags: item_text = f"{item_text} {filter_tags}"`
- Update `#stat-val-items` with `item_text`
- All other stat fields (rooms, containers, total_value) update unchanged from `get_stats()` result

Add `from typing import Optional` import at top if not already present.

Python 3.9 compatible: use `Optional[int]`, not `int | None`.

Do not change the widget's compose() structure, CSS, or any other method.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.tui.widgets.statsbar import StatsBar; import inspect; sig = inspect.signature(StatsBar.refresh_stats); assert 'item_count_override' in sig.parameters and 'filter_tags' in sig.parameters; print('StatsBar signature OK')"</automated>
  </verify>
  <done>
`StatsBar.refresh_stats()` accepts `item_count_override` and `filter_tags` parameters without breaking the existing signature. Imports cleanly.
  </done>
</task>

<task type="auto">
  <name>Task 2: Wire filter state, bindings, and picker callbacks in MainScreen</name>
  <files>possession/tui/screens/main.py</files>
  <action>
Make the following changes to `MainScreen` in `possession/tui/screens/main.py`:

**1. Add filter state fields in `__init__`** (after existing `self._filter_text` and similar fields):
```python
self._filter_room_id: Optional[int] = None
self._filter_room_name: Optional[str] = None
self._filter_container_id: Optional[int] = None
self._filter_container_name: Optional[str] = None
self._filter_category_id: Optional[int] = None
self._filter_category_name: Optional[str] = None
```

**2. Add bindings** to the existing `BINDINGS` list (add after `g` binding, before `a`):
```python
("r", "open_room_picker", "Room"),
("c", "open_container_picker", "Container"),
("t", "open_category_picker", "Category"),
```

**3. Update `_load_items()`** to pass filter IDs to `list_items()`:
```python
self._items = list_items(
    self.app.db_path,
    room_id=self._filter_room_id,
    container_id=self._filter_container_id,
    category_id=self._filter_category_id,
)
```
Keep the rest of `_load_items()` unchanged (table clear, add_columns, _apply_filter, _refresh_stats calls).

**4. Update `_refresh_stats()`** to pass filtered count and filter tags:
```python
def _refresh_stats(self) -> None:
    tags = self._build_filter_tags()
    self.query_one(StatsBar).refresh_stats(
        self.app.db_path,
        item_count_override=len(self._items),
        filter_tags=tags,
    )
```

**5. Add `_build_filter_tags()` method**:
```python
def _build_filter_tags(self) -> str:
    parts = []
    if self._filter_room_id is not None and self._filter_room_name:
        parts.append(f"[Room: {self._filter_room_name}]")
    if self._filter_container_id is not None and self._filter_container_name:
        parts.append(f"[Container: {self._filter_container_name}]")
    if self._filter_category_id is not None and self._filter_category_name:
        parts.append(f"[Category: {self._filter_category_name}]")
    return " ".join(parts)
```

**6. Add `_any_filter_active()` helper**:
```python
def _any_filter_active(self) -> bool:
    return any([
        self._filter_room_id is not None,
        self._filter_container_id is not None,
        self._filter_category_id is not None,
    ])
```

**7. Update `_apply_filter()`** to show empty state message when zero rows and filter active. After the item-adding loop, add:
```python
if table.row_count == 0 and self._any_filter_active():
    table.add_row(
        "No items match the current filters", "", "", "", "",
        key="__empty__",
    )
```
(Do this only if the table has columns already — place after the existing row-adding loop, not before.)

**8. Add action methods** for opening pickers:
```python
def action_open_room_picker(self) -> None:
    from possession.models import list_rooms
    from possession.tui.screens.filter_picker import FilterPickerScreen
    rooms = list_rooms(self.app.db_path)
    self.app.push_screen(
        FilterPickerScreen("Room", rooms, self._filter_room_id),
        self._on_room_picked,
    )

def action_open_container_picker(self) -> None:
    from possession.models import list_containers
    from possession.tui.screens.filter_picker import FilterPickerScreen
    containers = list_containers(self.app.db_path)
    self.app.push_screen(
        FilterPickerScreen("Container", containers, self._filter_container_id),
        self._on_container_picked,
    )

def action_open_category_picker(self) -> None:
    from possession.models import list_categories
    from possession.tui.screens.filter_picker import FilterPickerScreen
    categories = list_categories(self.app.db_path)
    self.app.push_screen(
        FilterPickerScreen("Category", categories, self._filter_category_id),
        self._on_category_picked,
    )
```

**9. Add picker callback methods**:
```python
def _on_room_picked(self, result: Optional[dict]) -> None:
    if result is None:
        return  # Escape — no change
    if result["id"] == self._filter_room_id:
        self._filter_room_id = None
        self._filter_room_name = None
    else:
        self._filter_room_id = result["id"]
        self._filter_room_name = result["name"]
    self._load_items()

def _on_container_picked(self, result: Optional[dict]) -> None:
    if result is None:
        return
    if result["id"] == self._filter_container_id:
        self._filter_container_id = None
        self._filter_container_name = None
    else:
        self._filter_container_id = result["id"]
        self._filter_container_name = result["name"]
    self._load_items()

def _on_category_picked(self, result: Optional[dict]) -> None:
    if result is None:
        return
    if result["id"] == self._filter_category_id:
        self._filter_category_id = None
        self._filter_category_name = None
    else:
        self._filter_category_id = result["id"]
        self._filter_category_name = result["name"]
    self._load_items()
```

**Important notes:**
- Use deferred imports inside action methods (same pattern as existing EditItemScreen usage in main.py)
- Python 3.9 compatible: use `Optional[dict]`, `Optional[int]`, `Optional[str]`
- `list_containers()` and `list_categories()` return dicts with `id` and `name` keys — confirm by reading models.py before writing. If the key names differ (e.g. `category_id` vs `id`), adapt accordingly.
- Do NOT remove any existing methods, bindings, or behavior — additive changes only
- The `r`/`c`/`t` keys should only fire when DataTable is focused. The existing `on_key` escape-priority ladder already prevents these from firing when `_filter_input_open` or `_quickadd_open` etc. are active — verify this is handled by checking existing on_key guard logic, and add a guard if not: `if self._any_input_active(): return` before the picker opens.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python -c "from possession.tui.screens.main import MainScreen; ms = MainScreen.__new__(MainScreen); print('MainScreen import OK')" && python -m py_compile possession/tui/screens/main.py && echo "syntax OK"</automated>
    <manual>Run `python -m possession` and press r — verify picker modal opens over the main screen with background dimmed. Select a room — verify item list narrows and stats bar shows '[Room: X]'. Press r again on same room — verify filter clears.</manual>
  </verify>
  <done>
`r`, `c`, `t` each open a FilterPickerScreen modal. Selection narrows the item list. Re-selecting active value clears it. Stats bar shows filtered count and active filter tags. Zero-match state shows the empty message row. Escape from picker makes no change.
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <what-built>
    Complete filter picker system: FilterPickerScreen modal, r/c/t bindings in MainScreen, StatsBar showing filtered count and tags, zero-match empty state, toggle-to-clear behavior.
  </what-built>
  <how-to-verify>
    1. Run `python -m possession` in `/Users/paritoshpatil/Personal/claude-projects/possession-terminal-2`
    2. Press `r` — a modal picker should appear centered over the screen with background dimmed
    3. Type a partial room name — list should filter live as you type
    4. Press `j`/`k` — cursor moves in list (not typing into search)
    5. Press Enter on a room — picker closes, item list narrows, stats bar shows `N items [Room: X]`
    6. Press `r` again — picker opens with active room marked `✓` at top, hint says "Enter to clear filter"
    7. Press Enter on the marked room — picker closes, filter cleared, all items return, filter tag gone
    8. Press `c` and `t` — same flow works for Container and Category
    9. Apply Room + Category filters simultaneously — both tags show in stats bar, list shows intersection
    10. Press Escape from picker — no change to current filters
    11. Apply a filter that matches zero items — DataTable shows "No items match the current filters" message, stats bar still shows filter tag
  </how-to-verify>
  <resume-signal>Type "approved" or describe any issues found</resume-signal>
</task>

</tasks>

<verification>
```bash
cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2
python -m py_compile possession/tui/screens/main.py && echo "main.py OK"
python -m py_compile possession/tui/widgets/statsbar.py && echo "statsbar.py OK"
python -m py_compile possession/tui/screens/filter_picker.py && echo "filter_picker.py OK"
python -c "from possession.tui.screens.main import MainScreen; from possession.tui.widgets.statsbar import StatsBar; from possession.tui.screens.filter_picker import FilterPickerScreen; print('All imports OK')"
```
</verification>

<success_criteria>
- All three filter keys (`r`/`c`/`t`) open the FilterPickerScreen modal
- Filter state stored in MainScreen, passed to list_items() on each load
- StatsBar shows `len(self._items)` (filtered) count, not global count
- Filter tags appear inline in stats bar when filters active
- Toggle behavior: selecting active value clears the filter
- Zero-match state shows message row in DataTable
- Escape from picker leaves filters unchanged
- No Python syntax errors, all files compile clean
</success_criteria>

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