---
phase: 02-browse
plan: 02
type: execute
wave: 2
depends_on: [02-01]
files_modified:
  - possession/tui/screens/main.py
  - possession/tui/widgets/__init__.py
  - possession/tui/widgets/breadcrumb.py
autonomous: true
requirements: [NAV-02, NAV-03, NAV-04]

must_haves:
  truths:
    - "Pressing / opens a filter bar at the bottom of the screen; typing narrows the DataTable rows in real time"
    - "Pressing Escape while the filter bar is active clears the filter and restores all rows"
    - "At the root level (no drill-down), the DataTable shows all items with all columns"
    - "Pressing Enter on a row when in item-list mode drills into that item's room (shows containers in that room)"
    - "After drilling into a room, pressing Enter on a container row shows only items in that container"
    - "Pressing q at any drill-down level goes back one level (container → room list, room → full item list)"
    - "A breadcrumb widget at the top of the screen shows the current drill-down path (e.g. 'All Items', 'Garage', 'Garage > Tool Cabinet')"
  artifacts:
    - path: "possession/tui/widgets/__init__.py"
      provides: "Widgets package marker"
    - path: "possession/tui/widgets/breadcrumb.py"
      provides: "Breadcrumb(Static) widget that renders current path string"
      exports: ["Breadcrumb"]
    - path: "possession/tui/screens/main.py"
      provides: "MainScreen with filter bar, drill-down state machine, and breadcrumb integration"
      min_lines: 140
  key_links:
    - from: "possession/tui/screens/main.py"
      to: "possession/tui/widgets/breadcrumb.py"
      via: "query_one(Breadcrumb).update(path_string)"
      pattern: "Breadcrumb"
    - from: "possession/tui/screens/main.py (filter bar Input)"
      to: "possession/tui/screens/main.py (DataTable)"
      via: "on_input_changed -> _apply_filter -> DataTable.clear + re-add rows"
      pattern: "on_input_changed|_apply_filter"
    - from: "possession/tui/screens/main.py (on_data_table_row_selected)"
      to: "possession.models (list_rooms, list_containers, list_items)"
      via: "drill-down state machine calling appropriate list_* function"
      pattern: "list_rooms|list_containers|list_items"
---

<objective>
Add live filtering, drill-down hierarchy navigation, and a breadcrumb trail to the main TUI screen.

Purpose: These three features together deliver the core browse experience. Filtering lets users find items instantly. Drill-down lets users navigate their physical space. Breadcrumbs give spatial context.

Output: A fully navigable TUI — users can filter by typing, drill into rooms and containers, and always know where they are.
</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/02-browse/02-01-SUMMARY.md

@possession/db.py
@possession/models.py
@possession/tui/screens/main.py
@possession/tui/app.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Live filter bar (NAV-02)</name>
  <files>
    possession/tui/screens/main.py
  </files>
  <action>
Extend `MainScreen` with a live filter Input widget.

**compose() change:** Add an `Input` widget below the DataTable with `placeholder="Filter... (/ to open, Esc to clear)"` and `id="filter-input"`. Initially hidden: `classes="hidden"`. CSS rule: `.hidden { display: none; }`.

**Open filter on `/` key:** Add `("/" , "open_filter", "Filter")` to BINDINGS. Action:
```python
def action_open_filter(self) -> None:
    inp = self.query_one("#filter-input", Input)
    inp.remove_class("hidden")
    inp.focus()
```

**Close/clear filter on Escape:** Handle in `on_key`:
```python
if event.key == "escape":
    inp = self.query_one("#filter-input", Input)
    if not inp.has_class("hidden"):
        inp.value = ""
        inp.add_class("hidden")
        self.query_one(DataTable).focus()
        self._apply_filter("")
        event.prevent_default()
        return
```

**on_input_changed handler:**
```python
def on_input_changed(self, event: Input.Changed) -> None:
    self._apply_filter(event.value)
```

**_apply_filter method:** Filter operates against `self._items` (the full list loaded in on_mount). Query is case-insensitive, matches against name, description, room_name, container_name, category_name fields.

```python
def _apply_filter(self, query: str) -> None:
    table = self.query_one(DataTable)
    table.clear()
    q = query.lower().strip()
    for item in self._items:
        if q and not any(
            q in (item.get(f) or "").lower()
            for f in ("name", "description", "room_name",
                      "container_name", "category_name")
        ):
            continue
        table.add_row(
            item["name"],
            item.get("description") or "",
            self._fmt_location(item),
            item.get("category_name") or "",
            self._fmt_cost(item.get("cost")),
            key=str(item["id"]),
        )
```

**Important:** `_apply_filter` must only be active when in the flat item-list view (not during drill-down). Add guard: `if self._view_mode != "items": return`. (`_view_mode` is introduced in Task 2.)

For now (before Task 2 exists), set `self._view_mode = "items"` as a default in `__init__` or `on_mount`. Task 2 will set it properly.

Python 3.9 compatible — use `Optional` from `typing`, not `X | None`.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python3 -c "
from possession.tui.screens.main import MainScreen
import inspect
src = inspect.getsource(MainScreen)
assert 'open_filter' in src, 'missing open_filter action'
assert '_apply_filter' in src, 'missing _apply_filter method'
assert 'on_input_changed' in src, 'missing on_input_changed handler'
print('PASS: filter bar methods present')
"</automated>
    <manual>None</manual>
    <sampling_rate>run after this task, before Task 2</sampling_rate>
  </verify>
  <done>
    MainScreen has action_open_filter, _apply_filter, and on_input_changed. Filter Input widget is in compose() with id="filter-input". Pressing / will show the filter bar; Escape will clear and hide it.
  </done>
</task>

<task type="auto">
  <name>Task 2: Drill-down state machine (NAV-03)</name>
  <files>
    possession/tui/screens/main.py
  </files>
  <action>
Add a drill-down state machine to MainScreen. The screen has three view modes: "rooms", "containers", "items". The default (root) is "items" (flat all-items view). Pressing Enter navigates down the hierarchy; pressing q goes back.

**State variables** (set in on_mount after loading items):
```python
self._view_mode: str = "items"   # "rooms" | "containers" | "items"
self._current_room_id: Optional[int] = None
self._current_room_name: str = ""
self._current_container_id: Optional[int] = None
self._current_container_name: str = ""
```

**_load_view() method** — central method that loads data for the current state and refreshes the DataTable:
```python
def _load_view(self) -> None:
    table = self.query_one(DataTable)
    table.clear()
    table.columns.clear()  # remove existing columns

    if self._view_mode == "rooms":
        table.add_columns("Room", "Containers")
        rooms = list_rooms(self.app.db_path)
        for room in rooms:
            containers = list_containers(self.app.db_path, room_id=room["id"])
            table.add_row(
                room["name"],
                str(len(containers)),
                key=str(room["id"]),
            )

    elif self._view_mode == "containers":
        table.add_columns("Container", "Items")
        containers = list_containers(self.app.db_path, room_id=self._current_room_id)
        for c in containers:
            items = list_items(self.app.db_path, container_id=c["id"])
            table.add_row(
                c["name"],
                str(len(items)),
                key=str(c["id"]),
            )

    else:  # "items"
        table.add_columns("Name", "Description", "Location", "Category", "Cost")
        items = list_items(
            self.app.db_path,
            room_id=self._current_room_id,
            container_id=self._current_container_id,
        )
        self._items = items
        self._apply_filter("")
```

**Note on columns.clear():** Textual's DataTable may not expose a `columns.clear()` method directly. If unavailable, rebuild the table by calling `table.clear(columns=True)` (Textual >= 0.47 supports `clear(columns=True)`) or by removing and re-adding the DataTable widget. Use `table.clear(columns=True)` as the primary approach; if that raises AttributeError, fall back to `table.clear()` then re-add same columns (acceptable since column counts differ between modes).

**on_mount change:** Replace the direct `list_items()` call + DataTable population with a call to `_load_view()`. Remove any column definition code from on_mount (it now lives in _load_view).

**Enter key — drill into selected row:**
```python
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
    row_key = event.row_key.value   # the string ID we passed as key=
    if self._view_mode == "items":
        # No drill-in from items in Phase 2
        # (Phase 3 will use this for edit)
        return
    elif self._view_mode == "rooms":
        room_id = int(row_key)
        rooms = list_rooms(self.app.db_path)
        room = next((r for r in rooms if r["id"] == room_id), None)
        if room:
            self._current_room_id = room_id
            self._current_room_name = room["name"]
            self._view_mode = "containers"
            self._load_view()
            self._update_breadcrumb()
    elif self._view_mode == "containers":
        container_id = int(row_key)
        containers = list_containers(self.app.db_path, room_id=self._current_room_id)
        container = next((c for c in containers if c["id"] == container_id), None)
        if container:
            self._current_container_id = container_id
            self._current_container_name = container["name"]
            self._view_mode = "items"
            self._load_view()
            self._update_breadcrumb()
```

**q key — go back one level:** Update the q BINDING action from `"app.pop_screen"` to `"go_back"`:
```python
("q", "go_back", "Back"),
```

```python
def action_go_back(self) -> None:
    if self._view_mode == "items" and self._current_container_id is not None:
        # Was in container drill-down → go back to containers list for this room
        self._current_container_id = None
        self._current_container_name = ""
        self._view_mode = "containers"
        self._load_view()
        self._update_breadcrumb()
    elif self._view_mode == "items" and self._current_room_id is not None:
        # Was in room drill-down (no container) → go back to rooms list
        self._current_room_id = None
        self._current_room_name = ""
        self._view_mode = "rooms"
        self._load_view()
        self._update_breadcrumb()
    elif self._view_mode == "containers":
        self._current_room_id = None
        self._current_room_name = ""
        self._view_mode = "rooms"
        self._load_view()
        self._update_breadcrumb()
    elif self._view_mode == "rooms":
        # Already at top — exit app
        self.app.exit()
```

**Initial view at startup:** Start in "rooms" view (not flat items view) — this gives the browse hierarchy from the start. Users navigate: rooms → containers → items. This matches the roadmap: "Pressing Enter on a room drills into its containers; pressing Enter on a container shows its items."

So set initial `self._view_mode = "rooms"` in on_mount before calling `_load_view()`.

Import additions at top of main.py:
```python
from possession.models import list_rooms, list_containers, list_items
```
(Replace any existing single `list_items` import.)

Python 3.9 compatible throughout. Use `Optional[int]` from `typing`.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python3 -c "
from possession.tui.screens.main import MainScreen
import inspect
src = inspect.getsource(MainScreen)
assert '_load_view' in src, 'missing _load_view'
assert 'on_data_table_row_selected' in src, 'missing row selected handler'
assert 'action_go_back' in src, 'missing go_back action'
assert '_view_mode' in src, 'missing _view_mode state'
assert 'list_rooms' in src, 'missing list_rooms call'
assert 'list_containers' in src, 'missing list_containers call'
print('PASS: drill-down state machine present')
"</automated>
    <manual>None</manual>
    <sampling_rate>run after this task, before Task 3</sampling_rate>
  </verify>
  <done>
    MainScreen has _view_mode state, _load_view(), on_data_table_row_selected(), and action_go_back(). The initial view on mount is "rooms". Enter on a room transitions to containers; Enter on a container transitions to items. q goes back one level.
  </done>
</task>

<task type="auto">
  <name>Task 3: Breadcrumb widget (NAV-04)</name>
  <files>
    possession/tui/widgets/__init__.py
    possession/tui/widgets/breadcrumb.py
    possession/tui/screens/main.py
  </files>
  <action>
Create a Breadcrumb widget and integrate it into MainScreen.

**possession/tui/widgets/__init__.py** — empty package marker.

**possession/tui/widgets/breadcrumb.py:**
```python
from textual.widget import Widget
from textual.app import ComposeResult
from textual.widgets import Static


class Breadcrumb(Static):
    """Displays the current drill-down navigation path."""

    DEFAULT_CSS = """
    Breadcrumb {
        height: 1;
        padding: 0 1;
        background: $primary-darken-2;
        color: $text;
    }
    """

    def __init__(self, path: str = "All Items", **kwargs):
        super().__init__(path, **kwargs)

    def set_path(self, path: str) -> None:
        """Update the displayed path string."""
        self.update(path)
```

**Integrate into MainScreen:**

1. Import at top of main.py:
```python
from possession.tui.widgets.breadcrumb import Breadcrumb
```

2. In `compose()`, yield Breadcrumb before the DataTable:
```python
def compose(self) -> ComposeResult:
    yield Breadcrumb(id="breadcrumb")
    yield DataTable(cursor_type="row", show_header=True)
    yield Input(placeholder="Filter... (/ to open, Esc to clear)",
                id="filter-input", classes="hidden")
```

3. Add `_update_breadcrumb()` method:
```python
def _update_breadcrumb(self) -> None:
    if self._view_mode == "rooms":
        path = "All Rooms"
    elif self._view_mode == "containers":
        path = self._current_room_name
    else:  # items
        if self._current_container_name:
            path = f"{self._current_room_name} > {self._current_container_name}"
        elif self._current_room_name:
            path = self._current_room_name
        else:
            path = "All Items"
    self.query_one(Breadcrumb).set_path(path)
```

4. Call `_update_breadcrumb()` at the end of `on_mount()` (after `_load_view()`).
5. `_update_breadcrumb()` is already called in on_data_table_row_selected and action_go_back (added in Task 2).

CSS layout update in app.py or MainScreen: add `height: 100%` to DataTable to fill remaining space after breadcrumb.

Python 3.9 compatible.
  </action>
  <verify>
    <automated>cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2 && python3 -c "
from possession.tui.widgets.breadcrumb import Breadcrumb
from possession.tui.screens.main import MainScreen
import inspect
src = inspect.getsource(MainScreen)
assert 'Breadcrumb' in src, 'Breadcrumb not imported in main.py'
assert '_update_breadcrumb' in src, 'missing _update_breadcrumb method'
assert 'set_path' in inspect.getsource(Breadcrumb), 'Breadcrumb missing set_path'
print('PASS: breadcrumb widget and integration present')
"</automated>
    <manual>None</manual>
    <sampling_rate>run after all tasks complete</sampling_rate>
  </verify>
  <done>
    possession/tui/widgets/breadcrumb.py exists with Breadcrumb(Static) class and set_path() method. MainScreen imports Breadcrumb, yields it in compose(), and calls _update_breadcrumb() in on_mount and on every view transition.
  </done>
</task>

</tasks>

<verification>
After all 3 tasks complete:

1. Full import chain:
```bash
cd /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2
python3 -c "
from possession.tui.app import PossessionApp
from possession.tui.screens.main import MainScreen
from possession.tui.widgets.breadcrumb import Breadcrumb
print('All imports OK')
"
```

2. Verify filter bar presence:
```bash
grep -n "_apply_filter\|open_filter\|on_input_changed" /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2/possession/tui/screens/main.py
```

3. Verify drill-down state machine:
```bash
grep -n "_view_mode\|_load_view\|action_go_back\|on_data_table_row_selected" /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2/possession/tui/screens/main.py
```

4. Verify breadcrumb:
```bash
grep -n "_update_breadcrumb\|Breadcrumb" /Users/paritoshpatil/Personal/claude-projects/possession-terminal-2/possession/tui/screens/main.py
```
</verification>

<success_criteria>
1. Pressing / in the running TUI opens the filter Input
2. Typing in the filter narrows rows in real time; Escape clears and closes it
3. The initial view shows all rooms in a two-column DataTable
4. Pressing Enter on a room shows its containers
5. Pressing Enter on a container shows items in that container
6. Pressing q returns to the previous level (container → room list → all rooms → exit)
7. The breadcrumb widget at top updates at every view transition
8. All imports succeed without errors
9. Python 3.9 compatible code throughout
</success_criteria>

<output>
After completion, create `.planning/phases/02-browse/02-02-SUMMARY.md` using the standard summary template.
</output>
