# Gitlore TUI — Product Requirements Document

**Author:** Work Ghost
**Date:** 2026-02-11
**Status:** DRAFT — Pending Approval
**Scope:** Interactive terminal UI for the `lore` CLI tool

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Framework Evaluation](#2-framework-evaluation)
3. [Recommendation](#3-recommendation)
4. [Architecture](#4-architecture)
5. [Screen Taxonomy](#5-screen-taxonomy)
6. [User Flows — Every Command](#6-user-flows)
7. [Widget Inventory](#7-widget-inventory)
8. [Keybinding Reference](#8-keybinding-reference)
9. [Implementation Plan](#9-implementation-plan)
10. [Code Changes Required](#10-code-changes-required)
11. [Assumptions](#11-assumptions)

---

## 1. Executive Summary

Gitlore (`lore`) is a 32,844-line Rust CLI for local GitLab data management with semantic search, people intelligence, and temporal analysis. It currently offers 21 commands via clap with two output modes: human-readable tables (`comfy-table`, `indicatif`, `console`) and machine-readable JSON (`--robot`).

**The problem:** Power users must memorize 21 commands, ~80 flags, and their interaction semantics. Discovering data relationships (who touched which files → which MRs → which discussions) requires sequential command invocations. There is no way to browse, filter, and drill into data interactively.

**The solution:** A full-screen interactive TUI that provides:

- **Dashboard** — sync status, project health, counts at a glance
- **Browsable entity lists** — issues, MRs, discussions with live filtering
- **Drill-down detail views** — threaded discussions, diff notes, cross-references
- **Integrated search** — hybrid FTS/semantic search with inline results
- **People explorer** — expert maps, workload snapshots, review patterns
- **Timeline viewer** — chronological event reconstruction with graph navigation
- **Sync orchestration** — progress visualization with real-time log streaming

The TUI augments but does not replace the existing CLI. All commands continue to work as-is.

---

## 2. Framework Evaluation

### 2.1 Candidates

| Dimension | FrankenTUI | Ratatui | Charm (bubbletea) |
|-----------|-----------|---------|-------------------|
| **Language** | Rust (nightly) | Rust (stable) | Go |
| **Age** | 6 days (Feb 2026) | 3+ years (fork of tui-rs) | 4+ years |
| **Downloads** | ~10 (crates.io) | 16.9M (crates.io) | N/A (Go module) |
| **Stars** | 125 | 18,200+ | 28,000+ |
| **Dependent crates** | 0 | 2,200+ | N/A |
| **Production users** | None known | Netflix, OpenAI, AWS, Vercel | Numerous Go tools |
| **Architecture** | Elm/Bubbletea | Immediate-mode | Elm/Bubbletea |
| **Inline mode** | First-class | App-specific hack | First-class (Bubble Tea) |
| **Layout** | Flex + Grid + Responsive breakpoints | Constraint-based Flex | Lipgloss (CSS-like) |
| **Widgets** | Rich (Table, Tree, CommandPalette, Modal, LogViewer, FilePicker, Charts) | Core set + community ecosystem | Bubbles (spinner, text input, list, paginator, table, viewport) |
| **Snapshot testing** | Built-in harness | None built-in | None built-in |
| **RAII teardown** | Kernel-enforced | App-specific | Deferred |
| **Theming** | WCAG-compliant, light/dark, semantic tokens | Manual style application | Lipgloss (adaptive colors) |
| **Accessibility** | WCAG contrast validation | None | None |
| **Toolchain req.** | Rust nightly | Rust stable | Go 1.18+ |
| **Documentation** | 76-94% (docs.rs) | Extensive tutorials + book | Excellent |

### 2.2 FrankenTUI — Deep Analysis

**Strengths:**
- **Inline mode is genuinely innovative.** Scrollback preservation while maintaining stable UI chrome is a killer feature for tools that mix streaming logs with interactive controls. This maps perfectly to `lore sync` where you want progress bars + log output.
- **Elm architecture is clean.** The `Model` trait with `update()/view()` and `Cmd<Msg>` for side effects is well-proven (Elm, Bubbletea, Iced). This is the right pattern for TUI state management.
- **Widget catalog is exceptionally rich for a v0.1.** CommandPalette, Tree, LogViewer, Modal, VirtualizedList, JsonView, Forms with validation — these are exactly what gitlore needs.
- **Responsive layout with breakpoints** handles the 80-col vs 200-col terminal reality gracefully.
- **WCAG contrast validation** is unique among Rust TUI frameworks.
- **Feature-gated crates** keep binary size controllable.
- **Snapshot testing harness** enables deterministic regression testing of TUI output.

**Risks (Critical):**
- **6 days old.** The API will change. There is no migration guide, no stability guarantee, no semver promise. Building on v0.1.1 means accepting frequent breakage.
- **10 downloads.** Zero production validation. Unknown edge cases in terminal compatibility, Unicode handling, resize behavior, SSH forwarding, tmux/screen, Windows Terminal, etc.
- **Requires Rust nightly.** Gitlore currently builds on stable Rust (edition 2024). Switching to nightly introduces compiler instability risk and makes CI more complex.
- **Solo maintainer.** Bus factor of 1. If Jeffrey Emanuel moves on, the ecosystem is orphaned.
- **No community.** Zero Stack Overflow answers, zero blog posts, zero third-party tutorials. Every problem is novel.
- **Partial crates.io publishing.** Not all crates are published yet — means depending on git source.

### 2.3 Ratatui — Deep Analysis

**Strengths:**
- **Battle-tested at scale.** 16.9M downloads, used by Netflix/OpenAI/AWS. Terminal compatibility is proven across macOS, Linux, Windows, tmux, SSH, Mosh, etc.
- **Stable Rust.** No toolchain change required for gitlore.
- **Massive ecosystem.** `tui-widgets` (33K downloads), `tui-textarea`, `tui-tree-widget`, `ratatui-image`, community templates, and 2,200+ dependent crates provide answers to almost any widget need.
- **Excellent documentation.** Official book, tutorials, examples repo with dozens of reference apps.
- **Constraint-based layout** is proven and well-understood.
- **Active development.** v0.30.0 with regular releases, multiple maintainers, responsive issue tracker.
- **Compatible with crossterm** (which gitlore already indirectly depends on via `console`/`dialoguer`).

**Limitations:**
- **No native inline mode.** Can be implemented app-side (alternating between raw/cooked mode) but it's not a first-class feature.
- **Immediate-mode rendering** means the app rebuilds the full widget tree every frame. For gitlore's data sizes this is fine (sub-millisecond), but it's a different mental model than Elm.
- **No built-in snapshot testing.** Would need to build or adopt `insta` snapshots.
- **Theming is manual.** No semantic token system or WCAG validation — you manage styles yourself.

### 2.4 Charm (bubbletea/lipgloss/bubbles) — Deep Analysis

**Eliminated.** Charm is Go-only. Gitlore is Rust. FFI bridging would be absurd. Including for completeness only because the user asked.

**However:** Charm's design philosophy heavily influenced FrankenTUI's architecture. The Elm pattern (`Model` + `Update` + `View`) and the inline-mode scrollback approach both originated in Charm's ecosystem. If gitlore were a Go project, Charm would be the obvious choice.

---

## 3. Recommendation

### Primary: Ratatui

**Use ratatui as the TUI framework.**

Rationale:
1. **Stability over novelty.** Gitlore is a production tool with real users (Taylor's workflow). Building on a 6-day-old framework with 10 downloads introduces unacceptable risk for a core user interface.
2. **No toolchain change.** Staying on stable Rust avoids nightly churn.
3. **Community safety net.** 2,200+ dependent crates means problems are solvable. Every terminal quirk has been encountered.
4. **Good enough widgets.** Between ratatui core, tui-widgets, and custom implementations, every gitlore screen can be built. The CommandPalette and Tree can be built in ~200 lines each.
5. **Proven at scale.** Netflix and OpenAI trust it. The terminal compatibility matrix is validated.

### Adopt FrankenTUI Ideas

While not using FrankenTUI as a dependency, we should steal its best ideas:

- **Elm-style state management:** Implement `Model + Message + update() + view()` as our own thin layer on ratatui. This is ~100 lines of code and gives us the clean architecture.
- **Inline mode for sync:** Use ratatui's `Terminal::insert_before()` (available since v0.28) to achieve scrollback-preserving inline mode during `lore sync`.
- **Semantic theming:** Build a `Theme` struct with named color slots that resolves against terminal capabilities.
- **Command palette:** Build a fuzzy-filter overlay that maps to lore commands.

### Revisit FrankenTUI in 6 Months

If FrankenTUI reaches v0.5+ with 1,000+ downloads, stable Rust support, and multiple production users, it would become a strong candidate for a v2 rewrite. The widget catalog and inline mode are genuinely superior.

---

## 4. Architecture

### 4.1 Module Structure

```
src/
  tui/                          # NEW: TUI module
    mod.rs                      # Public API: launch_tui()
    app.rs                      # App struct, event loop, Elm-style dispatch
    event.rs                    # Event polling (crossterm → Message translation)
    theme.rs                    # Semantic color tokens, light/dark detection
    message.rs                  # Message enum (all user actions + async results)
    state/
      mod.rs                    # AppState (top-level composition)
      dashboard.rs              # Dashboard state
      issue_list.rs             # Issue list + filter state
      issue_detail.rs           # Single issue view state
      mr_list.rs                # MR list + filter state
      mr_detail.rs              # Single MR view state
      search.rs                 # Search query + results state
      timeline.rs               # Timeline view state
      who.rs                    # People explorer state
      sync.rs                   # Sync progress state
      command_palette.rs        # Fuzzy command palette state
    view/
      mod.rs                    # Screen router (match state → render fn)
      dashboard.rs              # Dashboard layout + widgets
      issue_list.rs             # Issue table + filter bar
      issue_detail.rs           # Issue detail + threaded discussions
      mr_list.rs                # MR table + filter bar
      mr_detail.rs              # MR detail + diff notes
      search.rs                 # Search input + results + preview
      timeline.rs               # Event stream + entity graph
      who.rs                    # Expert map / workload / reviews
      sync.rs                   # Progress bars + log stream
      command_palette.rs        # Overlay fuzzy palette
      common/
        mod.rs                  # Shared widget helpers
        filter_bar.rs           # Reusable filter input row
        entity_table.rs         # Generic sortable/filterable table
        discussion_tree.rs      # Threaded discussion renderer
        status_bar.rs           # Bottom status + keybinding hints
        breadcrumb.rs           # Navigation breadcrumb trail
    action.rs                   # Async action runners (DB queries, GitLab calls)
    navigation.rs               # Screen stack + back/forward history
```

### 4.2 Elm-Style Event Loop

```mermaid
graph TD
    A[Terminal Events] -->|crossterm poll| B[Event Handler]
    B -->|translate| C[Message]
    C --> D[update fn]
    D -->|new state| E[AppState]
    D -->|side effects| F[Action Queue]
    F -->|spawn tokio task| G[Async Action]
    G -->|result| C
    E --> H[view fn]
    H -->|widget tree| I[ratatui Frame]
    I -->|diff + render| J[Terminal Output]

    style A fill:#2d2d2d,stroke:#666,color:#fff
    style C fill:#4a9eff,stroke:#333,color:#fff
    style D fill:#ff6b6b,stroke:#333,color:#fff
    style E fill:#51cf66,stroke:#333,color:#fff
    style H fill:#ffd43b,stroke:#333,color:#000
```

**Core types:**

```rust
// src/tui/message.rs

pub enum Message {
    // Navigation
    NavigateTo(Screen),
    GoBack,

    // Input
    Key(KeyEvent),
    Mouse(MouseEvent),
    Resize(u16, u16),
    Tick,

    // Command palette
    OpenCommandPalette,
    CloseCommandPalette,
    CommandPaletteInput(String),
    CommandPaletteSelect(usize),

    // Issue list
    IssueListLoaded(Vec<IssueRow>),
    IssueListFilterChanged(IssueFilter),
    IssueListSortChanged(SortField, SortOrder),
    IssueSelected(i64),  // gitlab iid

    // MR list
    MrListLoaded(Vec<MrRow>),
    MrListFilterChanged(MrFilter),
    MrSelected(i64),

    // Detail views
    IssueDetailLoaded(IssueDetail),
    MrDetailLoaded(MrDetail),
    DiscussionsLoaded(Vec<Discussion>),

    // Search
    SearchQueryChanged(String),
    SearchExecuted(SearchResults),
    SearchResultSelected(usize),

    // Timeline
    TimelineLoaded(Vec<TimelineEvent>),
    TimelineEntitySelected(EntityRef),

    // Who (people)
    WhoResultLoaded(WhoResult),
    WhoModeChanged(WhoMode),

    // Sync
    SyncStarted,
    SyncProgress(ProgressEvent),
    SyncLogLine(String),
    SyncCompleted(SyncResult),
    SyncFailed(String),

    // Dashboard
    DashboardLoaded(DashboardData),

    // System
    Error(String),
    Quit,
}

pub enum Screen {
    Dashboard,
    IssueList,
    IssueDetail(i64),  // project_id, iid
    MrList,
    MrDetail(i64),
    Search,
    Timeline,
    Who,
    Sync,
    Stats,
    Doctor,
}
```

```rust
// src/tui/app.rs

pub struct App {
    state: AppState,
    navigation: NavigationStack,
    action_tx: mpsc::UnboundedSender<Message>,
    db: Connection,
    config: Config,
    theme: Theme,
}

impl App {
    pub fn update(&mut self, msg: Message) -> Vec<Action> {
        match msg {
            Message::NavigateTo(screen) => {
                self.navigation.push(screen.clone());
                vec![Action::LoadScreen(screen)]
            }
            Message::GoBack => {
                if let Some(screen) = self.navigation.pop() {
                    vec![Action::LoadScreen(screen)]
                } else {
                    vec![]
                }
            }
            Message::Key(key) => self.handle_key(key),
            // ... delegate to sub-state handlers
            _ => self.delegate_to_active_screen(msg),
        }
    }

    pub fn view(&self, frame: &mut Frame) {
        let [header, body, footer] = Layout::vertical([
            Constraint::Length(1),   // breadcrumb
            Constraint::Min(0),      // main content
            Constraint::Length(1),   // status bar
        ]).areas(frame.area());

        // Breadcrumb
        view::common::breadcrumb::render(
            &self.navigation, frame, header
        );

        // Active screen
        match self.navigation.current() {
            Screen::Dashboard => view::dashboard::render(&self.state.dashboard, frame, body),
            Screen::IssueList => view::issue_list::render(&self.state.issue_list, frame, body),
            Screen::IssueDetail(_) => view::issue_detail::render(&self.state.issue_detail, frame, body),
            Screen::MrList => view::mr_list::render(&self.state.mr_list, frame, body),
            Screen::MrDetail(_) => view::mr_detail::render(&self.state.mr_detail, frame, body),
            Screen::Search => view::search::render(&self.state.search, frame, body),
            Screen::Timeline => view::timeline::render(&self.state.timeline, frame, body),
            Screen::Who => view::who::render(&self.state.who, frame, body),
            Screen::Sync => view::sync::render(&self.state.sync, frame, body),
            _ => {}
        }

        // Status bar with context-sensitive keybindings
        view::common::status_bar::render(
            &self.navigation, &self.theme, frame, footer
        );

        // Command palette overlay (if open)
        if self.state.command_palette.is_open {
            view::command_palette::render(
                &self.state.command_palette, frame, frame.area()
            );
        }
    }
}
```

### 4.3 Async Action System

```mermaid
graph LR
    subgraph "UI Thread (main)"
        A[App::update] -->|returns Actions| B[Action Dispatcher]
        E[Message Channel] --> A
    end

    subgraph "Tokio Runtime"
        B -->|spawn| C[DB Query]
        B -->|spawn| D[GitLab API Call]
        B -->|spawn| F[Embed Pipeline]
        C -->|msg_tx.send| E
        D -->|msg_tx.send| E
        F -->|msg_tx.send| E
    end

    style A fill:#ff6b6b,stroke:#333,color:#fff
    style E fill:#4a9eff,stroke:#333,color:#fff
    style C fill:#51cf66,stroke:#333,color:#fff
    style D fill:#51cf66,stroke:#333,color:#fff
    style F fill:#51cf66,stroke:#333,color:#fff
```

```rust
// src/tui/action.rs

pub enum Action {
    LoadScreen(Screen),

    // Database reads (run on blocking thread pool)
    FetchIssues(IssueFilter),
    FetchIssueDetail(i64),
    FetchMrs(MrFilter),
    FetchMrDetail(i64),
    FetchDiscussions(EntityType, i64),
    ExecuteSearch(SearchQuery),
    FetchTimeline(TimelineQuery),
    FetchWho(WhoQuery),
    FetchDashboard,
    FetchStats,

    // Network operations
    RunSync(SyncOptions),
    RunDoctor,

    // Browser
    OpenInBrowser(String),
}
```

### 4.4 Navigation Architecture

```mermaid
stateDiagram-v2
    [*] --> Dashboard
    Dashboard --> IssueList: i
    Dashboard --> MrList: m
    Dashboard --> Search: /
    Dashboard --> Timeline: t
    Dashboard --> Who: w
    Dashboard --> Sync: s

    IssueList --> IssueDetail: Enter
    IssueDetail --> IssueList: Esc/Backspace
    IssueDetail --> MrDetail: cross-ref link
    IssueDetail --> Timeline: t (scoped)

    MrList --> MrDetail: Enter
    MrDetail --> MrList: Esc/Backspace
    MrDetail --> IssueDetail: cross-ref link

    Search --> IssueDetail: Enter (issue result)
    Search --> MrDetail: Enter (MR result)

    Timeline --> IssueDetail: Enter (issue event)
    Timeline --> MrDetail: Enter (MR event)

    Who --> IssueList: Enter (person's issues)
    Who --> MrList: Enter (person's MRs)

    note right of Dashboard: Ctrl+P opens Command Palette from anywhere
    note right of Dashboard: Esc/Backspace always goes back
    note right of Dashboard: q quits from any screen
```

### 4.5 Data Flow — Read Path

```mermaid
sequenceDiagram
    participant U as User
    participant TUI as TUI Event Loop
    participant S as State
    participant A as Action Runner
    participant DB as SQLite

    U->>TUI: Press 'i' (go to issues)
    TUI->>S: Message::NavigateTo(IssueList)
    S->>S: Push to navigation stack
    S-->>A: Action::FetchIssues(default_filter)
    A->>DB: SELECT from issues WHERE...
    DB-->>A: Vec<IssueRow>
    A-->>TUI: Message::IssueListLoaded(rows)
    TUI->>S: Update issue_list.items
    TUI->>TUI: view() → render table
    TUI-->>U: Rendered issue list

    U->>TUI: Type in filter bar "bug"
    TUI->>S: Message::IssueListFilterChanged(label="bug")
    S-->>A: Action::FetchIssues(filter{label:"bug"})
    A->>DB: SELECT ... WHERE label = 'bug'
    DB-->>A: Filtered rows
    A-->>TUI: Message::IssueListLoaded(filtered)
    TUI-->>U: Updated table
```

---

## 5. Screen Taxonomy

### 5.1 Dashboard (Home Screen)

```
┌─ lore ─────────────────────────────────────────────────────────┐
│ Dashboard                                          Ctrl+P Help │
├────────────────────────────┬───────────────────────────────────┤
│  Projects (3)              │  Quick Stats                      │
│  ├─ vs/platform     ✓ 2m  │  Issues:  1,247 open / 3,891 tot │
│  ├─ vs/mobile-app   ✓ 5m  │  MRs:      89 open /   412 tot  │
│  └─ vs/infra        ⚠ 2h  │  Discuss: 14,293 threads         │
│                            │  Notes:   52,841 (12% system)    │
├────────────────────────────┤  Docs:     4,132 indexed         │
│  Last Sync                 │  Embeds:   3,891 vectors         │
│  Started: 2h 14m ago       ├───────────────────────────────────┤
│  Duration: 3m 42s          │  Recent Activity                  │
│  Issues: +12 new, 5 upd    │  ┌─ #1247 Fix auth timeout  12m │
│  MRs: +3 new, 8 upd       │  ├─ !456  Add caching layer 45m │
│  Discussions: +89 new      │  ├─ #1245 DB migration fail  1h │
│  Events: +234              │  ├─ !453  Refactor search    2h │
│  Errors: 0                 │  └─ #1244 Update deps        3h │
├────────────────────────────┴───────────────────────────────────┤
│ [i]ssues [m]rs [/]search [t]imeline [w]ho [s]ync [d]octor  [q]│
└────────────────────────────────────────────────────────────────┘
```

**Data source:** `lore count` + `lore sync-status` + recent issues/MRs query
**Update frequency:** On entry + every 60s tick
**Interaction:** Single-key navigation to all major screens

### 5.2 Issue List

```
┌─ lore > Issues ────────────────────────────────────────────────┐
│ Filter: state:opened  author:_  label:_  since:_     [Tab]edit │
├──────┬──────────────────────────────────┬────────┬─────────────┤
│  IID │ Title                            │ State  │ Updated     │
├──────┼──────────────────────────────────┼────────┼─────────────┤
│▶1247 │ Fix authentication timeout       │ opened │ 12 min ago  │
│ 1245 │ Database migration failure on ..  │ opened │ 1 hour ago  │
│ 1244 │ Update third-party dependencies  │ opened │ 3 hours ago │
│ 1243 │ Add rate limiting to public API  │ opened │ 5 hours ago │
│ 1241 │ Memory leak in worker process    │ opened │ 1 day ago   │
│ 1239 │ Dark mode color contrast issues  │ opened │ 2 days ago  │
│ 1237 │ Refactor notification system     │ opened │ 3 days ago  │
│ 1235 │ Add telemetry dashboard          │ opened │ 4 days ago  │
│ 1233 │ Fix flaky test in auth suite     │ closed │ 5 days ago  │
│ 1231 │ Support SAML SSO integration     │ opened │ 1 week ago  │
│  ... │                                  │        │             │
├──────┴──────────────────────────────────┴────────┴─────────────┤
│ 1/50  ↑↓ navigate  Enter detail  / filter  o open  Tab sort   │
└────────────────────────────────────────────────────────────────┘
```

**Data source:** `lore issues` query against SQLite
**Columns:** Configurable — iid, title, state, author, labels, milestone, updated_at
**Sorting:** Click column header or Tab to cycle (iid, updated, created)
**Filtering:** Interactive filter bar with field:value syntax
**Pagination:** Virtual scrolling for large result sets

### 5.3 Issue Detail

```
┌─ lore > Issues > #1247 ───────────────────────────────────────┐
│ Fix authentication timeout                    state: opened    │
│ Author: @asmith  Assignee: @bjones  Labels: bug, auth, P1     │
│ Milestone: v2.3  Created: 2026-02-08  Due: 2026-02-15         │
│ Project: vs/platform                                           │
├────────────────────────────────────────────────────────────────┤
│ ┌─ Description ──────────────────────────────────────────────┐ │
│ │ Users are experiencing authentication timeouts when the    │ │
│ │ session token refresh happens during a concurrent API      │ │
│ │ call. The race condition causes the refresh to fail and    │ │
│ │ the user gets logged out.                                  │ │
│ │                                                            │ │
│ │ Steps to reproduce:                                        │ │
│ │ 1. Login and wait for token to near expiry                 │ │
│ │ 2. Trigger multiple API calls simultaneously               │ │
│ │ 3. Observe 401 errors and forced logout                    │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ ┌─ Discussions (4 threads) ──────────────────────────────────┐ │
│ │ ▼ Thread 1 — @asmith (3 replies)                    2d ago │ │
│ │   I've narrowed this down to the TokenRefreshService.      │ │
│ │   The mutex isn't being held across the full refresh cy..  │ │
│ │   ├─ @bjones: Can you check if the retry logic in...  1d  │ │
│ │   ├─ @asmith: Yes, confirmed. The retry doesn't ch... 1d  │ │
│ │   └─ @clee: I have a fix in !458, PTAL             12h    │ │
│ │                                                            │ │
│ │ ▶ Thread 2 — @dkim (1 reply)                        1d ago │ │
│ │ ▶ Thread 3 — @system (label added: P1)             23h ago │ │
│ │ ▶ Thread 4 — @asmith (0 replies)                   12h ago │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ ┌─ Cross-References ─────────────────────────────────────────┐ │
│ │ Closes: !458 (Fix token refresh race condition)   opened   │ │
│ │ Related: #1198 (Session management rework)        closed   │ │
│ └────────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ Esc back  o open in browser  t timeline  Enter cross-ref      │
└────────────────────────────────────────────────────────────────┘
```

**Data source:** `lore issues <iid>` + discussions + cross-references
**Sections:** Collapsible (description, discussions, cross-refs)
**Discussion threads:** Expand/collapse with arrow keys
**Cross-reference navigation:** Enter on a ref → navigate to that entity
**Scrolling:** Vim-style (j/k) or arrow keys through content

### 5.4 MR List

Identical structure to Issue List with MR-specific columns:

| Column | Description |
|--------|-------------|
| IID | Merge request number |
| Title | MR title (draft prefix if WIP) |
| State | opened/merged/closed/locked |
| Draft | Draft indicator |
| Source | Source branch |
| Target | Target branch |
| Author | MR author |
| Updated | Relative time |

**Additional filters:** `--draft`, `--no-draft`, `--target-branch`, `--source-branch`, `--reviewer`

### 5.5 MR Detail

Similar to Issue Detail, with additional sections:

```
┌─ lore > MRs > !458 ──────────────────────────────────────────┐
│ Fix token refresh race condition              state: opened    │
│ Author: @clee  Reviewers: @asmith, @bjones  Labels: fix, auth │
│ Source: fix/token-refresh → Target: main                       │
│ Pipeline: ✓ passed  Conflicts: none                            │
├────────────────────────────────────────────────────────────────┤
│ ┌─ Description ──────────────────────────────────────────────┐ │
│ │ Fixes #1247. Holds the refresh mutex across the full       │ │
│ │ token lifecycle including retry.                            │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ ┌─ File Changes (3 files) ───────────────────────────────────┐ │
│ │  M src/auth/token_service.rs        +12 -3                 │ │
│ │  M src/auth/middleware.rs            +5  -2                 │ │
│ │  A tests/auth/token_refresh_test.rs +45                    │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ ┌─ Diff Discussions (2 threads) ─────────────────────────────┐ │
│ │ ▼ @asmith on src/auth/token_service.rs:45          12h ago │ │
│ │   "Should we add a timeout to the mutex lock here?"        │ │
│ │   └─ @clee: Good point, added in latest push          6h  │ │
│ │                                                            │ │
│ │ ▶ @bjones on src/auth/middleware.rs:23              10h ago │ │
│ └────────────────────────────────────────────────────────────┘ │
│                                                                │
│ ┌─ General Discussions (1 thread) ───────────────────────────┐ │
│ │ ▶ Thread 1 — @asmith: LGTM, approved               6h ago │ │
│ └────────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ Esc back  o open in browser  Enter cross-ref  f file changes  │
└────────────────────────────────────────────────────────────────┘
```

**Unique features:**
- **File changes list** with diffstat (+/- indicators)
- **Diff discussions** shown with file:line context
- **Separate sections** for diff discussions vs general discussions

### 5.6 Search

```
┌─ lore > Search ───────────────────────────────────────────────┐
│ Query: authentication timeout_                                 │
│ Mode: [hybrid]  Type: [all]  Since: [any]  Project: [all]     │
├───────────────────────────────────┬────────────────────────────┤
│  Results (24 matches, 42ms)       │  Preview                   │
│  ┌────────────────────────────┐   │  ┌──────────────────────┐  │
│  │▶ #1247 Fix auth timeout   │   │  │ Fix authentication    │  │
│  │  issue · vs/platform      │   │  │ timeout               │  │
│  │  Score: 0.94 (BM25+vec)   │   │  │                       │  │
│  │                            │   │  │ Users are experienc.. │  │
│  │  !458 Fix token refresh   │   │  │ ...session token       │  │
│  │  mr · vs/platform         │   │  │ refresh happens        │  │
│  │  Score: 0.87              │   │  │ during a concurrent    │  │
│  │                            │   │  │ API call...            │  │
│  │  #1198 Session mgmt       │   │  │                       │  │
│  │  issue · vs/platform      │   │  │ Labels: bug, auth, P1 │  │
│  │  Score: 0.72              │   │  │ Author: @asmith        │  │
│  │                            │   │  │ Updated: 12 min ago   │  │
│  │  Discussion on !412       │   │  │                       │  │
│  │  discussion · vs/platform │   │  │ ── Snippet ──          │  │
│  │  Score: 0.68              │   │  │ "...the [auth]         │  │
│  │                            │   │  │ [timeout] occurs       │  │
│  │  ...                       │   │  │ when the token..."     │  │
│  └────────────────────────────┘   │  └──────────────────────┘  │
├───────────────────────────────────┴────────────────────────────┤
│ ↑↓ select  Enter open  Tab switch mode  / refine  Esc back    │
└────────────────────────────────────────────────────────────────┘
```

**Layout:** Split pane — results list (left) + preview (right)
**Search modes:** Toggle between lexical / hybrid / semantic
**Filters:** Type (issue/mr/discussion), project, author, label, since
**Live preview:** Selected result shows snippet + metadata in right pane
**Debounced input:** 200ms debounce before executing search

### 5.7 Timeline

```
┌─ lore > Timeline ─────────────────────────────────────────────┐
│ Query: token refresh_                        Depth: 1  Since: 30d│
├────────────────────────────────────────────────────────────────┤
│  Seeds: #1247, !458, #1198                  Events: 47         │
├────────────────────────────────────────────────────────────────┤
│  Timeline                                                      │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ 2026-02-08 09:14  CREATED  #1247 Fix auth timeout       │  │
│  │                   by @asmith in vs/platform              │  │
│  │                                                          │  │
│  │ 2026-02-08 09:30  LABEL    #1247 +bug +auth             │  │
│  │                   by @asmith                             │  │
│  │                                                          │  │
│  │ 2026-02-08 14:22  NOTE     #1247 "I've narrowed this    │  │
│  │                   down to the TokenRefreshService..."    │  │
│  │                   by @asmith                             │  │
│  │                                                          │  │
│  │ 2026-02-09 10:00  CREATED  !458 Fix token refresh race  │  │
│  │                   by @clee (closes #1247)                │  │
│  │                                                          │  │
│  │ 2026-02-09 10:05  XREF     !458 → #1247 (closes)       │  │
│  │                                                          │  │
│  │ 2026-02-09 16:30  LABEL    #1247 +P1                    │  │
│  │                   by @lead                               │  │
│  │                                                          │  │
│  │ 2026-02-10 08:00  NOTE     !458 "@asmith PTAL"          │  │
│  │                   by @clee [src/auth/token_service.rs]   │  │
│  │                                                          │  │
│  │ 2026-02-10 14:00  STATE    !458 approved                │  │
│  │                   by @asmith                             │  │
│  └──────────────────────────────────────────────────────────┘  │
├────────────────────────────────────────────────────────────────┤
│ ↑↓ scroll  Enter entity  d depth  s since  / search  Esc back │
└────────────────────────────────────────────────────────────────┘
```

**Data source:** Timeline pipeline (SEED → HYDRATE → EXPAND → COLLECT → RENDER)
**Event types:** Color-coded by type (CREATED=green, STATE=yellow, LABEL=cyan, NOTE=white, XREF=magenta, MERGED=green+bold)
**Interaction:** Enter on any event → navigate to the parent entity
**Controls:** Adjust depth (BFS expansion), since window, max seeds

### 5.8 People Explorer (Who)

```
┌─ lore > Who ──────────────────────────────────────────────────┐
│ Mode: [Expert]  Path: src/auth/_              Since: 6m       │
├────────────────────────────────────────────────────────────────┤
│  Expert Rankings for src/auth/                                 │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │  Rank │ Person   │ Score │ Authored │ Reviewed │ Notes   │  │
│  │ ─────┼──────────┼───────┼──────────┼──────────┼──────── │  │
│  │▶  1  │ @asmith  │   142 │    8 MRs │    5 MRs │  23     │  │
│  │   2  │ @bjones  │    89 │    3 MRs │   12 MRs │  15     │  │
│  │   3  │ @clee    │    67 │    5 MRs │    2 MRs │   8     │  │
│  │   4  │ @dkim    │    34 │    1 MR  │    4 MRs │   5     │  │
│  │   5  │ @epark   │    21 │    2 MRs │    1 MR  │   3     │  │
│  └──────────────────────────────────────────────────────────┘  │
│                                                                │
│  @asmith — Detail                                              │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ Top code areas:                                          │  │
│  │   src/auth/token_service.rs    ████████████░  45%        │  │
│  │   src/auth/middleware.rs       ██████░░░░░░░  22%        │  │
│  │   src/auth/session.rs          ████░░░░░░░░░  15%        │  │
│  │   src/auth/oauth.rs            ██░░░░░░░░░░░   8%        │  │
│  │   other                        ██░░░░░░░░░░░  10%        │  │
│  └──────────────────────────────────────────────────────────┘  │
├────────────────────────────────────────────────────────────────┤
│ Tab mode  Enter person MRs  / path  r reviews  a active       │
└────────────────────────────────────────────────────────────────┘
```

**Modes (Tab to cycle):**

| Mode | Description | Data Source |
|------|-------------|-------------|
| Expert | Path-based expert ranking | `lore who <path>` |
| Workload | Person's assigned work | `lore who @username` |
| Reviews | DiffNote review patterns | `lore who @username --reviews` |
| Active | Unresolved discussions | `lore who --active` |
| Overlap | Who else touches files | `lore who --overlap <path>` |

### 5.9 Sync

```
┌─ lore > Sync ─────────────────────────────────────────────────┐
│ Status: Syncing...  Elapsed: 1m 23s                            │
├────────────────────────────────────────────────────────────────┤
│  vs/platform                                                   │
│  ├─ Issues     ████████████████████████████████░░ 89%  +12    │
│  ├─ MRs        ████████████████████████████████████ 100% +3   │
│  ├─ Discussions ██████████████░░░░░░░░░░░░░░░░░░ 42%  +89    │
│  └─ Events     ████████████████████░░░░░░░░░░░░ 58%  +234    │
│                                                                │
│  vs/mobile-app                                                 │
│  ├─ Issues     ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0%   queued │
│  ├─ MRs        ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0%   queued │
│  ├─ Discussions ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0%   queued │
│  └─ Events     ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0%   queued │
│                                                                │
│  vs/infra                                                      │
│  └─ (queued)                                                   │
├────────────────────────────────────────────────────────────────┤
│  Log ──────────────────────────────────────────────────────────│
│  │ [12:01:23] Fetching issues page 4/5 for vs/platform        │
│  │ [12:01:24] Upserting 20 issues (12 new, 8 updated)         │
│  │ [12:01:25] Fetching discussions for 12 issues              │
│  │ [12:01:26] Rate limit: 28/30 req/s, backing off 200ms     │
│  │ [12:01:27] Fetching resource events page 1/? for vs/plat  │
│  │                                                             │
├────────────────────────────────────────────────────────────────┤
│ Esc cancel  f full sync  e embed after  d dry-run  l log level│
└────────────────────────────────────────────────────────────────┘
```

**Layout:** Progress bars (top) + scrolling log (bottom)
**Progress source:** `ProgressCallback` / `ProgressEvent` from ingestion orchestrator
**Log source:** Captured tracing subscriber output
**Controls:**
- `f` — toggle full sync (reset cursors)
- `e` — toggle embed after sync
- `d` — dry-run mode
- `l` — cycle log verbosity
- `Esc` — cancel running sync (graceful shutdown via signal)

### 5.10 Command Palette (Overlay)

```
┌─────────────────────────────────────────────────────┐
│ > sync fu_                                           │
├─────────────────────────────────────────────────────┤
│ ▶ Sync (full)          Reset cursors and re-sync    │
│   Sync                 Incremental sync             │
│   Sync (no embed)      Skip embedding step          │
│   Search "fu"          Search for "fu" in docs      │
└─────────────────────────────────────────────────────┘
```

**Trigger:** `Ctrl+P` from any screen
**Behavior:** Fuzzy match against all commands + recent entities
**Commands mapped:**

| Palette Entry | Action |
|---------------|--------|
| Issues | Navigate to Issue List |
| Issues (opened) | Issue List pre-filtered |
| Issues (closed) | Issue List pre-filtered |
| Merge Requests | Navigate to MR List |
| MRs (opened) | MR List pre-filtered |
| MRs (merged) | MR List pre-filtered |
| MRs (draft) | MR List pre-filtered |
| Search | Navigate to Search |
| Timeline | Navigate to Timeline |
| Who (expert) | Who → Expert mode |
| Who (workload) | Who → Workload mode |
| Who (active) | Who → Active mode |
| Sync | Start incremental sync |
| Sync (full) | Full sync with cursor reset |
| Sync (no embed) | Sync without embedding |
| Doctor | Run health check |
| Stats | Show index statistics |
| Open in browser | Open current entity in GitLab |
| Quit | Exit TUI |

### 5.11 Doctor / Stats (Info Screens)

Simple read-only views rendering the output of `lore doctor` and `lore stats` as styled text blocks. No complex interaction needed — just scrollable content with Esc to go back.

---

## 6. User Flows

### 6.1 Flow: "Find who knows about auth code"

```mermaid
graph TD
    A[Dashboard] -->|w| B[Who Screen]
    B -->|type 'src/auth/'| C[Expert Mode Results]
    C -->|↓ select @asmith| D[Detail Panel Shows Code Areas]
    D -->|Enter| E[Issue List filtered by @asmith]
    E -->|↓ select #1247| F[Issue Detail]
    F -->|scroll to discussions| G[Read discussion threads]
    G -->|Enter on cross-ref !458| H[MR Detail]
    H -->|o| I[Opens in browser]

    style A fill:#2d2d2d,stroke:#666,color:#fff
    style C fill:#4a9eff,stroke:#333,color:#fff
    style F fill:#51cf66,stroke:#333,color:#fff
    style H fill:#ffd43b,stroke:#333,color:#000
```

**Keystrokes:** `w` → type `src/auth/` → `↓` → `Enter` → `↓` → `Enter` → `↓↓↓` → `Enter` → `o`
**Total:** 8 actions to go from "who knows about auth" to opening the relevant MR in browser.
**CLI equivalent:** `lore who src/auth/` → read output → `lore who @asmith` → read → `lore issues 1247` → read → `lore mrs 458` → `lore mrs 458 --open`
**CLI keystrokes:** 6 separate commands, ~200 characters of typing.

### 6.2 Flow: "What happened with the auth timeout this week?"

```mermaid
graph TD
    A[Dashboard] -->|t| B[Timeline Screen]
    B -->|type 'auth timeout'| C[Timeline Seeds: #1247, !458]
    C -->|scroll through events| D[See chronological story]
    D -->|Enter on CREATED !458| E[MR Detail]
    E -->|Esc| D
    D -->|Enter on NOTE evidence| F[Issue Detail at discussion]

    style A fill:#2d2d2d,stroke:#666,color:#fff
    style C fill:#4a9eff,stroke:#333,color:#fff
    style D fill:#51cf66,stroke:#333,color:#fff
```

**Keystrokes:** `t` → type query → `Enter` → scroll → `Enter` → `Esc` → scroll
**Value:** The timeline view reconstructs the full narrative that would require 5+ separate CLI commands.

### 6.3 Flow: "Quick search for something I vaguely remember"

```mermaid
graph TD
    A[Any Screen] -->|/| B[Search Screen]
    B -->|type query| C[Live Results + Preview]
    C -->|Tab| D[Switch to semantic mode]
    D -->|↑↓ browse| E[Preview updates in real-time]
    E -->|Enter| F[Entity Detail View]

    style B fill:#4a9eff,stroke:#333,color:#fff
    style C fill:#51cf66,stroke:#333,color:#fff
    style F fill:#ffd43b,stroke:#333,color:#000
```

### 6.4 Flow: "Sync and check results"

```mermaid
graph TD
    A[Dashboard] -->|s| B[Sync Screen]
    B -->|Enter to start| C[Progress Bars + Log Stream]
    C -->|watch progress| D[Sync Completes]
    D -->|i| E[Issue List shows new items]
    E -->|sort by updated| F[See freshly synced issues]

    style B fill:#4a9eff,stroke:#333,color:#fff
    style C fill:#ff6b6b,stroke:#333,color:#fff
    style D fill:#51cf66,stroke:#333,color:#fff
```

### 6.5 Flow: "Review someone's open work"

```mermaid
graph TD
    A[Dashboard] -->|w| B[Who Screen]
    B -->|Tab to Workload| C[Workload Mode]
    C -->|type '@bjones'| D[Workload Summary]
    D -->|see assigned issues| E[Issue section]
    D -->|see authored MRs| F[MR section]
    D -->|see review queue| G[Review section]
    E -->|Enter| H[Issue Detail]
    F -->|Enter| I[MR Detail]

    style D fill:#4a9eff,stroke:#333,color:#fff
    style E fill:#51cf66,stroke:#333,color:#fff
    style F fill:#51cf66,stroke:#333,color:#fff
    style G fill:#51cf66,stroke:#333,color:#fff
```

### 6.6 Flow: Command Palette Power Usage

```mermaid
graph TD
    A[Any Screen] -->|Ctrl+P| B[Command Palette Opens]
    B -->|type 'mrs draft'| C[Fuzzy matches 'MRs draft']
    C -->|Enter| D[MR List pre-filtered: draft=true]
    D -->|Ctrl+P again| E[Palette Opens]
    E -->|type 'sync'| F[Fuzzy matches Sync commands]
    F -->|↓ select 'Sync full'| G[Sync Screen starts full sync]

    style B fill:#ffd43b,stroke:#333,color:#000
    style C fill:#4a9eff,stroke:#333,color:#fff
    style E fill:#ffd43b,stroke:#333,color:#000
```

---

## 7. Widget Inventory

### 7.1 Ratatui Built-in Widgets Used

| Widget | Usage | Screen(s) |
|--------|-------|-----------|
| `Table` | Issue/MR lists, expert rankings, file changes | IssueList, MrList, Who, MrDetail |
| `Paragraph` | Descriptions, discussion content, log output | IssueDetail, MrDetail, Sync |
| `Block` | Section borders with titles | All |
| `Gauge` | Sync progress bars | Sync |
| `List` | Search results, timeline events, command palette | Search, Timeline, Palette |
| `Sparkline` | Activity trends on dashboard | Dashboard |
| `Tabs` | Mode switching (Who modes, Search modes) | Who, Search |
| `Scrollbar` | Long content scrolling | Detail views, Timeline |
| `BarChart` | Code area distribution | Who (expert detail) |

### 7.2 Custom Widgets (Build Ourselves)

| Widget | Purpose | Complexity |
|--------|---------|------------|
| `FilterBar` | Interactive field:value filter input | Medium |
| `DiscussionTree` | Threaded discussion with collapse/expand | High |
| `CommandPalette` | Fuzzy-filtered command overlay | Medium |
| `Breadcrumb` | Navigation trail showing current path | Low |
| `StatusBar` | Context-sensitive keybinding hints | Low |
| `EntityPreview` | Right-pane preview of selected entity | Medium |
| `ProgressPanel` | Multi-project sync progress with log tail | Medium |
| `TimelineStream` | Color-coded chronological event list | Medium |
| `CrossRefLink` | Clickable entity references | Low |

### 7.3 Third-Party Widget Crates

| Crate | Widget | Version | Purpose |
|-------|--------|---------|---------|
| `tui-textarea` | TextArea | latest | Multi-line search/filter input |
| `tui-tree-widget` | Tree | latest | Discussion thread hierarchy |
| `tui-scrollview` | ScrollView | latest | Smooth scrolling for detail views |

---

## 8. Keybinding Reference

### 8.1 Global (Available Everywhere)

| Key | Action |
|-----|--------|
| `q` | Quit TUI |
| `Esc` | Go back / close overlay |
| `Backspace` | Go back (when not in text input) |
| `Ctrl+P` | Open command palette |
| `/` | Focus search (or navigate to Search from Dashboard) |
| `?` | Show help overlay |
| `o` | Open current entity in browser |
| `Ctrl+C` | Quit (force) |

### 8.2 List Screens (Issues, MRs, Search Results)

| Key | Action |
|-----|--------|
| `j` / `↓` | Move selection down |
| `k` / `↑` | Move selection up |
| `Enter` | Open selected item |
| `G` | Jump to bottom |
| `g` | Jump to top |
| `Tab` | Cycle sort column |
| `f` | Focus filter bar |
| `r` | Refresh data |
| `[` / `]` | Previous/next page |

### 8.3 Detail Screens (Issue Detail, MR Detail)

| Key | Action |
|-----|--------|
| `j` / `↓` | Scroll down |
| `k` / `↑` | Scroll up |
| `Space` | Page down |
| `b` | Page up |
| `d` | Toggle discussion collapse/expand |
| `Enter` | Follow cross-reference link |
| `t` | Open timeline scoped to this entity |

### 8.4 Dashboard

| Key | Action |
|-----|--------|
| `i` | Go to Issues |
| `m` | Go to MRs |
| `/` | Go to Search |
| `t` | Go to Timeline |
| `w` | Go to Who |
| `s` | Go to Sync |
| `d` | Run Doctor |
| `r` | Refresh dashboard |

### 8.5 Who Screen

| Key | Action |
|-----|--------|
| `Tab` | Cycle mode (Expert → Workload → Reviews → Active → Overlap) |
| `Enter` | Open person's issues/MRs |
| `r` | Switch to Reviews sub-view |
| `a` | Switch to Active sub-view |

---

## 9. Implementation Plan

### 9.1 Dependency Additions

```toml
# Cargo.toml additions

[dependencies]
# TUI framework
ratatui = { version = "0.30", features = ["crossterm"] }
crossterm = "0.28"

# TUI widgets
tui-textarea = "0.7"
tui-tree-widget = "0.22"
tui-scrollview = "0.5"

# Fuzzy matching for command palette
nucleo-matcher = "0.3"
```

### 9.2 Phases

```mermaid
gantt
    title TUI Implementation Phases
    dateFormat  YYYY-MM-DD
    axisFormat  %b %d

    section Phase 1 — Foundation
    App skeleton + event loop       :p1a, 2026-02-15, 3d
    Theme system                    :p1b, after p1a, 1d
    Navigation stack + breadcrumb   :p1c, after p1a, 2d
    Status bar + keybinding hints   :p1d, after p1c, 1d

    section Phase 2 — Core Screens
    Dashboard                       :p2a, after p1d, 2d
    Issue List + filter bar         :p2b, after p2a, 3d
    Issue Detail + discussions      :p2c, after p2b, 4d
    MR List                         :p2d, after p2b, 1d
    MR Detail + diff discussions    :p2e, after p2d, 3d

    section Phase 3 — Power Features
    Search (hybrid + preview)       :p3a, after p2e, 3d
    Timeline viewer                 :p3b, after p3a, 3d
    Who (all 5 modes)              :p3c, after p3b, 4d
    Command Palette                 :p3d, after p2a, 2d

    section Phase 4 — Operations
    Sync screen + progress          :p4a, after p3c, 3d
    Doctor + Stats views            :p4b, after p4a, 1d
    CLI integration (lore tui)      :p4c, after p4b, 1d

    section Phase 5 — Polish
    Responsive breakpoints          :p5a, after p4c, 2d
    Snapshot tests                  :p5b, after p5a, 2d
    Documentation                   :p5c, after p5b, 1d
```

**Total estimated scope:** ~40 implementation days across 5 phases.

### 9.3 Phase 1 Detail — Foundation

**Goal:** Render a blank TUI with working navigation, theme, and event loop.

**Deliverables:**
1. `src/tui/mod.rs` — `pub fn launch_tui(config: Config, db_path: &Path) -> Result<()>`
2. `src/tui/app.rs` — `App` struct with Elm-style `update()` + `view()`
3. `src/tui/event.rs` — Crossterm event polling on background thread → `Message` channel
4. `src/tui/message.rs` — Full `Message` enum
5. `src/tui/navigation.rs` — `NavigationStack` with push/pop/current
6. `src/tui/theme.rs` — Semantic theme tokens (detect dark/light terminal)
7. `src/tui/view/common/breadcrumb.rs` — Navigation breadcrumb renderer
8. `src/tui/view/common/status_bar.rs` — Keybinding hint bar
9. `src/main.rs` — Add `lore tui` subcommand

---

## 10. Code Changes Required

### 10.1 New CLI Subcommand

```rust
// src/cli/mod.rs — add to Commands enum

#[derive(Subcommand)]
pub enum Commands {
    // ... existing commands ...

    /// Launch interactive TUI
    Tui,
}
```

```rust
// src/main.rs — add match arm

Commands::Tui => {
    let config = load_config(&cli)?;
    let db_path = config.storage.db_path();
    crate::tui::launch_tui(config, &db_path)?;
}
```

### 10.2 Refactor: Extract Query Functions

Currently, some commands execute queries and format output in the same function. For the TUI, we need the query layer separated from the display layer.

**Pattern to apply across all commands:**

```rust
// BEFORE (coupled)
pub fn run_list_issues(config: &Config, args: &IssueArgs) -> Result<()> {
    let conn = open_db(&config)?;
    let rows = query_issues(&conn, &args.filter)?;
    print_issue_table(&rows, args.robot);
    Ok(())
}

// AFTER (decoupled)
pub fn query_issues(conn: &Connection, filter: &IssueFilter) -> Result<Vec<IssueRow>> {
    // Pure data query, no I/O
}

pub fn run_list_issues(config: &Config, args: &IssueArgs) -> Result<()> {
    let conn = open_db(&config)?;
    let rows = query_issues(&conn, &args.filter)?;
    print_issue_table(&rows, args.robot);
    Ok(())
}
```

**Files requiring this refactor:**

| File | Extract | Used By |
|------|---------|---------|
| `src/cli/commands/list.rs` | `query_issues()`, `query_mrs()` | IssueList, MrList |
| `src/cli/commands/show.rs` | `query_issue_detail()`, `query_mr_detail()`, `query_discussions()` | IssueDetail, MrDetail |
| `src/cli/commands/search.rs` (if exists) | `execute_search()` | Search |
| `src/cli/commands/who.rs` | `query_experts()`, `query_workload()`, `query_reviews()` | Who |
| Timeline modules | Already pipeline-based (good) | Timeline |
| Ingestion orchestrator | Already has `ProgressCallback` (good) | Sync |

### 10.3 Core App Structure

```rust
// src/tui/mod.rs

mod app;
mod event;
mod message;
mod navigation;
mod theme;
mod action;
mod state;
mod view;

use std::path::Path;
use crate::core::config::Config;
use crate::core::error::LoreError;

pub fn launch_tui(config: Config, db_path: &Path) -> Result<(), LoreError> {
    // Enable raw mode
    crossterm::terminal::enable_raw_mode()
        .map_err(|e| LoreError::Io(e.into()))?;

    // Enter alternate screen
    let mut stdout = std::io::stdout();
    crossterm::execute!(
        stdout,
        crossterm::terminal::EnterAlternateScreen,
        crossterm::event::EnableMouseCapture,
    ).map_err(|e| LoreError::Io(e.into()))?;

    let backend = ratatui::backend::CrosstermBackend::new(stdout);
    let mut terminal = ratatui::Terminal::new(backend)
        .map_err(|e| LoreError::Io(e.into()))?;

    // Build runtime
    let rt = tokio::runtime::Runtime::new()
        .map_err(|e| LoreError::Io(e.into()))?;

    // Run app
    let result = rt.block_on(async {
        let db = crate::core::db::create_connection(db_path)?;
        let mut app = app::App::new(config, db);
        app.run(&mut terminal).await
    });

    // Restore terminal (RAII-safe)
    crossterm::terminal::disable_raw_mode()
        .map_err(|e| LoreError::Io(e.into()))?;
    crossterm::execute!(
        terminal.backend_mut(),
        crossterm::terminal::LeaveAlternateScreen,
        crossterm::event::DisableMouseCapture,
    ).map_err(|e| LoreError::Io(e.into()))?;
    terminal.show_cursor()
        .map_err(|e| LoreError::Io(e.into()))?;

    result
}
```

```rust
// src/tui/event.rs

use crossterm::event::{self, Event as CrosstermEvent, KeyCode, KeyModifiers};
use tokio::sync::mpsc;
use std::time::Duration;

pub struct EventHandler {
    rx: mpsc::UnboundedReceiver<CrosstermEvent>,
}

impl EventHandler {
    pub fn new() -> (Self, tokio::task::JoinHandle<()>) {
        let (tx, rx) = mpsc::unbounded_channel();
        let handle = tokio::task::spawn_blocking(move || {
            loop {
                if event::poll(Duration::from_millis(50)).unwrap_or(false) {
                    if let Ok(evt) = event::read() {
                        if tx.send(evt).is_err() {
                            break;
                        }
                    }
                }
            }
        });
        (Self { rx }, handle)
    }

    pub async fn next(&mut self) -> Option<CrosstermEvent> {
        self.rx.recv().await
    }
}
```

```rust
// src/tui/theme.rs

use ratatui::style::{Color, Modifier, Style};

pub struct Theme {
    // Semantic tokens
    pub fg: Color,
    pub fg_dim: Color,
    pub fg_emphasis: Color,
    pub bg: Color,
    pub bg_surface: Color,
    pub bg_highlight: Color,

    // Entity colors
    pub issue_opened: Color,
    pub issue_closed: Color,
    pub mr_opened: Color,
    pub mr_merged: Color,
    pub mr_closed: Color,
    pub mr_draft: Color,

    // Event colors (timeline)
    pub event_created: Color,
    pub event_state: Color,
    pub event_label: Color,
    pub event_note: Color,
    pub event_xref: Color,
    pub event_merged: Color,

    // UI chrome
    pub border: Color,
    pub border_focus: Color,
    pub selection_bg: Color,
    pub selection_fg: Color,
    pub search_match: Color,
    pub error: Color,
    pub warning: Color,
    pub success: Color,

    // Progress
    pub progress_fill: Color,
    pub progress_empty: Color,
}

impl Theme {
    pub fn dark() -> Self {
        Self {
            fg: Color::Rgb(220, 220, 220),
            fg_dim: Color::Rgb(128, 128, 128),
            fg_emphasis: Color::White,
            bg: Color::Reset,  // Use terminal default
            bg_surface: Color::Rgb(30, 30, 30),
            bg_highlight: Color::Rgb(45, 45, 45),

            issue_opened: Color::Green,
            issue_closed: Color::Red,
            mr_opened: Color::Green,
            mr_merged: Color::Rgb(130, 80, 220),
            mr_closed: Color::Red,
            mr_draft: Color::Yellow,

            event_created: Color::Green,
            event_state: Color::Yellow,
            event_label: Color::Cyan,
            event_note: Color::White,
            event_xref: Color::Magenta,
            event_merged: Color::Green,

            border: Color::Rgb(60, 60, 60),
            border_focus: Color::Rgb(100, 150, 255),
            selection_bg: Color::Rgb(50, 80, 140),
            selection_fg: Color::White,
            search_match: Color::Rgb(255, 200, 50),
            error: Color::Red,
            warning: Color::Yellow,
            success: Color::Green,

            progress_fill: Color::Rgb(100, 150, 255),
            progress_empty: Color::Rgb(40, 40, 40),
        }
    }

    pub fn detect() -> Self {
        // Future: detect light/dark from terminal
        Self::dark()
    }

    // Convenience style builders
    pub fn title(&self) -> Style {
        Style::default().fg(self.fg_emphasis).add_modifier(Modifier::BOLD)
    }

    pub fn dim(&self) -> Style {
        Style::default().fg(self.fg_dim)
    }

    pub fn selected(&self) -> Style {
        Style::default().fg(self.selection_fg).bg(self.selection_bg)
    }

    pub fn state_color(&self, state: &str) -> Color {
        match state {
            "opened" => self.issue_opened,
            "closed" => self.issue_closed,
            "merged" => self.mr_merged,
            "locked" => self.warning,
            _ => self.fg,
        }
    }
}
```

```rust
// src/tui/navigation.rs

use super::message::Screen;

pub struct NavigationStack {
    stack: Vec<Screen>,
}

impl NavigationStack {
    pub fn new() -> Self {
        Self {
            stack: vec![Screen::Dashboard],
        }
    }

    pub fn current(&self) -> &Screen {
        self.stack.last().expect("navigation stack is never empty")
    }

    pub fn push(&mut self, screen: Screen) {
        self.stack.push(screen);
    }

    pub fn pop(&mut self) -> Option<Screen> {
        if self.stack.len() > 1 {
            self.stack.pop()
        } else {
            None // Can't pop the last screen
        }
    }

    pub fn breadcrumbs(&self) -> Vec<&str> {
        self.stack.iter().map(|s| s.label()).collect()
    }

    pub fn depth(&self) -> usize {
        self.stack.len()
    }

    pub fn reset_to(&mut self, screen: Screen) {
        self.stack.clear();
        self.stack.push(screen);
    }
}

impl Screen {
    pub fn label(&self) -> &str {
        match self {
            Screen::Dashboard => "Dashboard",
            Screen::IssueList => "Issues",
            Screen::IssueDetail(_) => "Issue",
            Screen::MrList => "MRs",
            Screen::MrDetail(_) => "MR",
            Screen::Search => "Search",
            Screen::Timeline => "Timeline",
            Screen::Who => "Who",
            Screen::Sync => "Sync",
            Screen::Stats => "Stats",
            Screen::Doctor => "Doctor",
        }
    }
}
```

### 10.4 Sample View Implementation — Dashboard

```rust
// src/tui/view/dashboard.rs

use ratatui::{
    Frame,
    layout::{Constraint, Layout, Rect},
    style::{Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Gauge, List, ListItem, Paragraph, Sparkline},
};
use crate::tui::state::dashboard::DashboardState;
use crate::tui::theme::Theme;

pub fn render(state: &DashboardState, theme: &Theme, frame: &mut Frame, area: Rect) {
    let [left, right] = Layout::horizontal([
        Constraint::Percentage(40),
        Constraint::Percentage(60),
    ]).areas(area);

    let [projects, sync_info] = Layout::vertical([
        Constraint::Percentage(50),
        Constraint::Percentage(50),
    ]).areas(left);

    let [stats, recent] = Layout::vertical([
        Constraint::Length(10),
        Constraint::Min(0),
    ]).areas(right);

    // Projects panel
    render_projects(state, theme, frame, projects);

    // Sync info panel
    render_sync_info(state, theme, frame, sync_info);

    // Quick stats panel
    render_stats(state, theme, frame, stats);

    // Recent activity panel
    render_recent(state, theme, frame, recent);
}

fn render_projects(state: &DashboardState, theme: &Theme, frame: &mut Frame, area: Rect) {
    let block = Block::default()
        .title(" Projects ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border));

    let items: Vec<ListItem> = state.projects.iter().map(|p| {
        let status_icon = if p.minutes_since_sync < 10 {
            Span::styled(" ✓ ", Style::default().fg(theme.success))
        } else if p.minutes_since_sync < 120 {
            Span::styled(" ⚠ ", Style::default().fg(theme.warning))
        } else {
            Span::styled(" ✗ ", Style::default().fg(theme.error))
        };

        let age = format_relative_time(p.minutes_since_sync);

        ListItem::new(Line::from(vec![
            Span::raw("  "),
            status_icon,
            Span::styled(&p.path, Style::default().fg(theme.fg)),
            Span::raw("  "),
            Span::styled(age, theme.dim()),
        ]))
    }).collect();

    let list = List::new(items).block(block);
    frame.render_widget(list, area);
}

fn render_stats(state: &DashboardState, theme: &Theme, frame: &mut Frame, area: Rect) {
    let block = Block::default()
        .title(" Quick Stats ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border));

    let text = vec![
        Line::from(vec![
            Span::styled("  Issues:    ", theme.dim()),
            Span::styled(
                format!("{} open", state.counts.issues_open),
                Style::default().fg(theme.issue_opened),
            ),
            Span::styled(
                format!(" / {} total", state.counts.issues_total),
                theme.dim(),
            ),
        ]),
        Line::from(vec![
            Span::styled("  MRs:       ", theme.dim()),
            Span::styled(
                format!("{} open", state.counts.mrs_open),
                Style::default().fg(theme.mr_opened),
            ),
            Span::styled(
                format!(" / {} total", state.counts.mrs_total),
                theme.dim(),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Discuss:   ", theme.dim()),
            Span::styled(
                format!("{} threads", state.counts.discussions),
                Style::default().fg(theme.fg),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Notes:     ", theme.dim()),
            Span::styled(
                format!("{}", state.counts.notes_total),
                Style::default().fg(theme.fg),
            ),
            Span::styled(
                format!(" ({}% system)", state.counts.notes_system_pct),
                theme.dim(),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Documents: ", theme.dim()),
            Span::styled(
                format!("{} indexed", state.counts.documents),
                Style::default().fg(theme.fg),
            ),
        ]),
        Line::from(vec![
            Span::styled("  Embeddings:", theme.dim()),
            Span::styled(
                format!("{} vectors", state.counts.embeddings),
                Style::default().fg(theme.fg),
            ),
        ]),
    ];

    let paragraph = Paragraph::new(text).block(block);
    frame.render_widget(paragraph, area);
}

fn render_recent(state: &DashboardState, theme: &Theme, frame: &mut Frame, area: Rect) {
    let block = Block::default()
        .title(" Recent Activity ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(theme.border));

    let items: Vec<ListItem> = state.recent.iter().map(|item| {
        let prefix = match item.entity_type.as_str() {
            "issue" => Span::styled(
                format!("  #{:<5}", item.iid),
                Style::default().fg(theme.issue_opened),
            ),
            "mr" => Span::styled(
                format!("  !{:<5}", item.iid),
                Style::default().fg(theme.mr_opened),
            ),
            _ => Span::raw("       "),
        };

        let title = Span::styled(
            truncate_str(&item.title, 40),
            Style::default().fg(theme.fg),
        );

        let age = Span::styled(
            format!("  {}", format_relative_time(item.minutes_ago)),
            theme.dim(),
        );

        ListItem::new(Line::from(vec![prefix, title, age]))
    }).collect();

    let list = List::new(items).block(block);
    frame.render_widget(list, area);
}

fn format_relative_time(minutes: u64) -> String {
    if minutes < 60 {
        format!("{}m ago", minutes)
    } else if minutes < 1440 {
        format!("{}h ago", minutes / 60)
    } else {
        format!("{}d ago", minutes / 1440)
    }
}

fn truncate_str(s: &str, max: usize) -> String {
    if s.len() > max {
        format!("{}..", &s[..s.floor_char_boundary(max - 2)])
    } else {
        s.to_string()
    }
}
```

### 10.5 Command Palette Implementation

```rust
// src/tui/state/command_palette.rs

use nucleo_matcher::{Matcher, pattern::Pattern};

pub struct CommandPaletteState {
    pub is_open: bool,
    pub input: String,
    pub items: Vec<PaletteItem>,
    pub filtered: Vec<usize>,  // Indices into items
    pub selected: usize,
    matcher: Matcher,
}

pub struct PaletteItem {
    pub label: String,
    pub description: String,
    pub action: PaletteAction,
}

pub enum PaletteAction {
    Navigate(super::super::message::Screen),
    RunSync { full: bool, embed: bool },
    RunDoctor,
    ShowStats,
    OpenBrowser,
    Quit,
}

impl CommandPaletteState {
    pub fn new() -> Self {
        let items = vec![
            PaletteItem {
                label: "Issues".into(),
                description: "Browse all issues".into(),
                action: PaletteAction::Navigate(Screen::IssueList),
            },
            PaletteItem {
                label: "Issues (opened)".into(),
                description: "Issues filtered to opened state".into(),
                action: PaletteAction::Navigate(Screen::IssueList),
            },
            PaletteItem {
                label: "Merge Requests".into(),
                description: "Browse all merge requests".into(),
                action: PaletteAction::Navigate(Screen::MrList),
            },
            PaletteItem {
                label: "MRs (draft)".into(),
                description: "Draft/WIP merge requests".into(),
                action: PaletteAction::Navigate(Screen::MrList),
            },
            PaletteItem {
                label: "Search".into(),
                description: "Hybrid full-text + semantic search".into(),
                action: PaletteAction::Navigate(Screen::Search),
            },
            PaletteItem {
                label: "Timeline".into(),
                description: "Chronological event reconstruction".into(),
                action: PaletteAction::Navigate(Screen::Timeline),
            },
            PaletteItem {
                label: "Who (expert)".into(),
                description: "Find experts for a code path".into(),
                action: PaletteAction::Navigate(Screen::Who),
            },
            PaletteItem {
                label: "Who (workload)".into(),
                description: "View someone's assigned work".into(),
                action: PaletteAction::Navigate(Screen::Who),
            },
            PaletteItem {
                label: "Who (active)".into(),
                description: "Unresolved discussions, last 7 days".into(),
                action: PaletteAction::Navigate(Screen::Who),
            },
            PaletteItem {
                label: "Sync".into(),
                description: "Incremental sync from GitLab".into(),
                action: PaletteAction::RunSync { full: false, embed: true },
            },
            PaletteItem {
                label: "Sync (full)".into(),
                description: "Full sync with cursor reset".into(),
                action: PaletteAction::RunSync { full: true, embed: true },
            },
            PaletteItem {
                label: "Sync (no embed)".into(),
                description: "Sync without vector embedding".into(),
                action: PaletteAction::RunSync { full: false, embed: false },
            },
            PaletteItem {
                label: "Doctor".into(),
                description: "Check environment health".into(),
                action: PaletteAction::RunDoctor,
            },
            PaletteItem {
                label: "Stats".into(),
                description: "Document and index statistics".into(),
                action: PaletteAction::ShowStats,
            },
            PaletteItem {
                label: "Open in browser".into(),
                description: "Open current entity in GitLab".into(),
                action: PaletteAction::OpenBrowser,
            },
            PaletteItem {
                label: "Quit".into(),
                description: "Exit the TUI".into(),
                action: PaletteAction::Quit,
            },
        ];

        let filtered = (0..items.len()).collect();

        Self {
            is_open: false,
            input: String::new(),
            items,
            filtered,
            selected: 0,
            matcher: Matcher::new(nucleo_matcher::Config::DEFAULT),
        }
    }

    pub fn open(&mut self) {
        self.is_open = true;
        self.input.clear();
        self.selected = 0;
        self.update_filter();
    }

    pub fn close(&mut self) {
        self.is_open = false;
    }

    pub fn update_filter(&mut self) {
        if self.input.is_empty() {
            self.filtered = (0..self.items.len()).collect();
        } else {
            // Fuzzy match using nucleo
            let pattern = Pattern::parse(
                &self.input,
                nucleo_matcher::pattern::CaseMatching::Ignore,
                nucleo_matcher::pattern::Normalization::Smart,
            );

            let mut scored: Vec<(usize, u32)> = self.items.iter()
                .enumerate()
                .filter_map(|(i, item)| {
                    let mut buf = Vec::new();
                    let haystack = nucleo_matcher::Utf32Str::new(
                        &item.label, &mut buf
                    );
                    pattern.score(haystack, &mut self.matcher)
                        .map(|score| (i, score))
                })
                .collect();

            scored.sort_by(|a, b| b.1.cmp(&a.1));
            self.filtered = scored.into_iter().map(|(i, _)| i).collect();
        }

        self.selected = 0;
    }

    pub fn select_current(&self) -> Option<&PaletteAction> {
        self.filtered.get(self.selected)
            .map(|&i| &self.items[i].action)
    }
}
```

---

## Appendix A: Full Architecture Diagram

```mermaid
graph TB
    subgraph "Terminal Layer"
        CT[crossterm]
        RT[ratatui]
    end

    subgraph "TUI Module (new)"
        direction TB
        EH[Event Handler] -->|Message| AL[App Loop]
        AL -->|update| ST[State Tree]
        AL -->|view| VW[View Router]
        AL -->|actions| AR[Action Runner]

        subgraph "State"
            ST --> DS[Dashboard]
            ST --> IL[Issue List]
            ST --> ID[Issue Detail]
            ST --> ML[MR List]
            ST --> MD[MR Detail]
            ST --> SS[Search]
            ST --> TL[Timeline]
            ST --> WH[Who]
            ST --> SY[Sync]
            ST --> CP[Command Palette]
        end

        subgraph "Views"
            VW --> DV[Dashboard View]
            VW --> IV[Issue Views]
            VW --> MV[MR Views]
            VW --> SV[Search View]
            VW --> TV[Timeline View]
            VW --> WV[Who View]
            VW --> YV[Sync View]
            VW --> PV[Palette Overlay]
        end
    end

    subgraph "Existing Modules (unchanged)"
        direction TB
        DB[(SQLite DB)]
        GL[GitLab Client]
        ING[Ingestion]
        DOC[Documents]
        EMB[Embedding]
        SCH[Search]
        TIM[Timeline]
        WHO2[Who Queries]
    end

    CT -->|events| EH
    VW -->|widgets| RT
    AR -->|queries| DB
    AR -->|sync| ING
    AR -->|search| SCH
    AR -->|timeline| TIM
    AR -->|who| WHO2
    ING -->|fetch| GL
    ING -->|store| DB
    DOC -->|index| DB
    EMB -->|vectors| DB

    style AL fill:#ff6b6b,stroke:#333,color:#fff
    style ST fill:#4a9eff,stroke:#333,color:#fff
    style VW fill:#51cf66,stroke:#333,color:#fff
    style DB fill:#ffd43b,stroke:#333,color:#000
```

---

## Appendix B: State Machine — Filter Bar

```mermaid
stateDiagram-v2
    [*] --> Inactive

    Inactive --> Active: f (focus) or Tab
    Active --> FieldSelect: type ':'
    Active --> FreeText: type any char

    FieldSelect --> ValueInput: select field
    FreeText --> Inactive: Enter (apply) or Esc (cancel)

    ValueInput --> Inactive: Enter (apply filter)
    ValueInput --> Active: Backspace past ':'

    state Active {
        [*] --> Typing
        Typing --> Suggesting: pause 200ms
        Suggesting --> Typing: resume typing
    }

    state FieldSelect {
        [*] --> ShowFields
        ShowFields: state, author, assignee, label, milestone, since, project
    }

    state ValueInput {
        [*] --> ShowValues
        ShowValues: context-dependent completions
    }
```

---

## Appendix C: Responsive Layout Breakpoints

| Terminal Width | Layout Adaptation |
|---------------|-------------------|
| < 80 cols | Single-column, abbreviated headers, hide secondary columns |
| 80-120 cols | Standard layout (as shown in mockups) |
| 120-200 cols | Wider columns, show all fields, split pane previews |
| > 200 cols | Triple-column layout on search/who screens |

**Implementation:** Check `frame.area().width` in each view function and adjust `Constraint` arrays accordingly.

---

## Appendix D: Discussion Thread Rendering Algorithm

```mermaid
graph TD
    A[Fetch discussions for entity] --> B{For each discussion}
    B --> C[Get root note]
    C --> D[Format thread header: author + age + reply count]
    D --> E{Expanded?}
    E -->|Yes| F[Render root note body]
    F --> G{For each reply}
    G --> H[Indent + render reply: author + body + age]
    H --> G
    E -->|No| I[Show collapsed indicator with preview]
    B --> B

    style A fill:#4a9eff,stroke:#333,color:#fff
    style F fill:#51cf66,stroke:#333,color:#fff
    style I fill:#ffd43b,stroke:#333,color:#000
```

**For diff discussions (MR-specific):**
1. Group by file path
2. Within each file, sort by line number
3. Show `[file.rs:45]` context prefix
4. Indent discussion tree under file header

---

## 11. Assumptions

### Framework Choice

1. **FrankenTUI will stabilize within 6 months.** If it doesn't reach v0.5+ with broader adoption, we never revisit. If it does, we evaluate a migration.
2. **Ratatui 0.30 API is stable.** We assume no breaking changes in the 0.30.x line during implementation.
3. **crossterm works in all target terminals.** macOS Terminal.app, iTerm2, Alacritty, Kitty, tmux, SSH sessions. This is well-validated by ratatui's adoption.
4. **Nightly Rust is unacceptable for gitlore.** This eliminates FrankenTUI as a direct dependency.

### Data & Performance

5. **SQLite queries are fast enough for interactive use.** Issue lists with 10,000+ items should render filtered results in <50ms. The existing index design supports this.
6. **Virtual scrolling handles large datasets.** We don't load all 10K issues into memory — we page from SQLite.
7. **Sync progress can be captured as events.** The existing `ProgressCallback` / `ProgressEvent` system is sufficient for the TUI progress display.
8. **The database is never locked during TUI use.** Only `lore sync` acquires the app lock. The TUI reads with WAL mode, which allows concurrent reads.

### User Experience

9. **Vim-style keybindings are acceptable.** `j/k` navigation is standard for developer TUIs. We also support arrow keys for accessibility.
10. **Mouse support is optional but nice.** Click-to-select in lists, scrollbar dragging. Not required for core functionality.
11. **The TUI does not replace the CLI.** All commands continue to work via `lore issues`, `lore search`, etc. The TUI is invoked explicitly via `lore tui`.
12. **No TUI-specific configuration is needed initially.** Theme auto-detects dark/light. Keybindings are fixed (no customization in v1).
13. **Users have 80+ column terminals.** The minimum usable width is ~60 columns. Below that, we show a "terminal too narrow" message.

### Implementation

14. **The query layer can be cleanly extracted.** Some commands may have coupled query+display logic that needs refactoring. This is estimated at ~2 days of work.
15. **tokio is already in the dependency tree.** No new async runtime needed — we use the existing tokio runtime for async actions.
16. **No new SQLite tables are needed.** The TUI is purely a view layer over existing data.
17. **The TUI module is feature-gated.** `cargo build --features tui` to avoid adding ratatui/crossterm to the default binary size for robot-mode-only deployments.

### Scope Boundaries

18. **No write operations in v1.** The TUI is read-only + sync. No creating issues, editing labels, or posting comments from the TUI. This is a significant simplification.
19. **No `init` wizard in the TUI.** Configuration remains CLI-only (`lore init`).
20. **No embedding visualization.** The TUI shows embedding counts but doesn't visualize vector space.
21. **No concurrent TUI sessions.** Only one `lore tui` instance at a time (enforced by terminal ownership, not app locks).

---

## Sources

- [FrankenTUI GitHub Repository](https://github.com/Dicklesworthstone/frankentui)
- [FrankenTUI Website](https://www.frankentui.com/)
- [FrankenTUI on LinkedIn (Jeffrey Emanuel)](https://www.linkedin.com/posts/jeffreyemanuel_frankentui-the-monster-terminal-ui-kernel-activity-7426409327512514560-pNEx)
- [Ratatui Official Site](https://ratatui.rs/)
- [Ratatui crates.io](https://crates.io/crates/ratatui)
- [FrankenTUI Hacker News Discussion](https://news.ycombinator.com/item?id=46911912)
- [ftui-extras docs.rs](https://docs.rs/ftui-extras/latest/aarch64-apple-darwin/ftui_extras/)
- [ftui-widgets docs.rs](https://docs.rs/ftui-widgets/latest/ftui_widgets/)
- [ftui-runtime docs.rs](https://docs.rs/ftui-runtime/latest/ftui_runtime/)
- [ftui-layout docs.rs](https://docs.rs/ftui-layout/latest/ftui_layout/)
- [ftui-core docs.rs](https://docs.rs/ftui-core/latest/ftui_core/)
- [ftui-style docs.rs](https://docs.rs/ftui-style/latest/ftui_style/)
- [ftui-demo-showcase on crates.io](https://crates.io/crates/ftui-demo-showcase)
- [@doodlestein on X announcing FrankenTUI](https://x.com/doodlestein/status/2018848938141614302)
