# 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` 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), IssueListFilterChanged(IssueFilter), IssueListSortChanged(SortField, SortOrder), IssueSelected(i64), // gitlab iid // MR list MrListLoaded(Vec), MrListFilterChanged(MrFilter), MrSelected(i64), // Detail views IssueDetailLoaded(IssueDetail), MrDetailLoaded(MrDetail), DiscussionsLoaded(Vec), // Search SearchQueryChanged(String), SearchExecuted(SearchResults), SearchResultSelected(usize), // Timeline TimelineLoaded(Vec), 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, db: Connection, config: Config, theme: Theme, } impl App { pub fn update(&mut self, msg: Message) -> Vec { 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 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 ` + 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 ` | | 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 ` | ### 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> { // 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, } 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 { 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, } 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 { 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 = 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 = 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, pub filtered: Vec, // 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)