specs/004-dashboard-filtering/plan.md
Ref: Size: 4.2 KiB
# Implementation Plan: Dashboard Filtering
**Branch**: `004-dashboard-filtering` | **Date**: 2026-03-21 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/004-dashboard-filtering/spec.md`
## Summary
Add text search and status filter cycling to the TUI dashboard. Users press `/` to enter search mode with real-time case-insensitive title filtering, cycle status filters (open/closed/all) with the existing `a` key, and see active filter state in the footer. All changes are confined to `src/tui.rs` — no new dependencies or modules required.
## Technical Context
**Language/Version**: Rust 2021 edition
**Primary Dependencies**: ratatui 0.30, crossterm 0.29, git2 0.19
**Storage**: N/A (ephemeral filter state, no persistence)
**Testing**: cargo test (integration tests in `tests/`)
**Target Platform**: Terminal (Linux/macOS)
**Project Type**: CLI tool with TUI dashboard
**Performance Goals**: Immediate keystroke response (sub-frame, <16ms)
**Constraints**: Single-threaded event loop, no async
**Scale/Scope**: ~1000 lines in tui.rs, adding ~100-150 lines
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
Constitution is unpopulated (template only). No gates to enforce. User preference for TDD noted from memory — tests should be written before implementation.
## Project Structure
### Documentation (this feature)
```text
specs/004-dashboard-filtering/
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── spec.md # Feature specification
├── checklists/ # Quality checklists
└── tasks.md # Phase 2 output (via /speckit.tasks)
```
### Source Code (repository root)
```text
src/
├── tui.rs # PRIMARY: All filtering logic added here
└── state.rs # READ ONLY: IssueStatus, PatchStatus enums used for filtering
tests/
├── common/ # Shared test helpers
└── [existing tests] # No new test files needed; TUI filtering tested via unit tests in src/tui.rs
```
**Structure Decision**: Single-file change in `src/tui.rs`. The `App` struct gains new fields (`search_query: String`, `search_active: bool`, `status_filter: StatusFilter`). The existing `show_all: bool` is replaced by a `StatusFilter` enum (Open, Closed, All). Visible item methods (`visible_issues`, `visible_patches`) are extended to apply both text and status filters. The `render_footer` function is updated to show active filter state. A new `InputMode` enum or flag manages whether keystrokes go to search input vs normal navigation.
## Design Decisions
### 1. Search Mode State Machine
Add an `InputMode` concept to `App`:
- **Normal**: All existing keybindings work as before
- **Search**: Keystrokes append to `search_query`; only Escape, Backspace, and printable chars are handled
The `/` key transitions Normal → Search. Escape transitions Search → Normal and clears the query.
### 2. Status Filter Enum Replaces `show_all` Boolean
```
enum StatusFilter { Open, Closed, All }
```
The `a` key cycles: Open → All → Closed → Open. This replaces the current `show_all: bool` toggle. The list title shows the current filter (e.g., "Issues (closed)").
### 3. Filter Application in Visible Methods
`visible_issues()` and `visible_patches()` apply both filters:
1. Status filter (match on `IssueStatus`/`PatchStatus`)
2. Text filter (case-insensitive substring match on `title`)
Both filters compose — an item must pass both to be visible.
### 4. Footer Rendering
When search is active: footer shows `Search: {query}_` with a cursor indicator.
When search is inactive but a status filter other than the default (Open) is active: footer shows the active status filter in the hint area.
Normal mode with no special filter: existing key hints shown.
### 5. Selection Reset on Filter Change
Any filter change (typing, status cycle) checks if the current selection is still within bounds. If not, resets to index 0 or None if the list is empty.
### 6. Key Conflict Check
Verified: `/` is not bound to any existing action in `run_loop`. The `a` key is already used for `show_all` toggle and will be repurposed for the status filter cycle.