Comprehensive product requirements document for the gitlore TUI built on
FrankenTUI's Elm architecture (Msg -> update -> view). The PRD (7800+
lines) covers:
Architecture: Separate binary crate (lore-tui) with runtime delegation,
Elm-style Model/Cmd/Msg, DbManager with closure-based read pool + WAL,
TaskSupervisor for dedup/cancellation, EntityKey system for type-safe
entity references, CommandRegistry as single source of truth for
keybindings/palette/help.
Screens: Dashboard, IssueList, IssueDetail, MrList, MrDetail, Search
(lexical/hybrid/semantic with facets), Timeline (5-stage pipeline),
Who (expert/workload/reviews/active/overlap), Sync (live progress),
CommandPalette, Help overlay.
Infrastructure: InputMode state machine, Clock trait for deterministic
rendering, crash_context ring buffer with redaction, instance lock,
progressive hydration, session restore, grapheme-safe text truncation
(unicode-width + unicode-segmentation), terminal sanitization (ANSI/bidi/
C1 controls), entity LRU cache.
Testing: Snapshot tests via insta, event-fuzz, CLI/TUI parity, tiered
benchmark fixtures (S/M/L), query-plan CI enforcement, Phase 2.5
vertical slice gate.
9 plan-refine iterations (ChatGPT review -> Claude integration):
Iter 1-3: Connection pool, debounce, EntityKey, TaskSupervisor,
keyset pagination, capability-adaptive rendering
Iter 4-6: Separate binary crate, ANSI hardening, session restore,
read tx isolation, progressive hydration, unicode-width
Iter 7-9: Per-screen LoadState, CommandRegistry, InputMode, Clock,
log redaction, entity cache, search cancel SLO, crash diagnostics
Also includes the original tui-prd.md (ratatui-based, superseded by v2).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2076 lines
85 KiB
Markdown
2076 lines
85 KiB
Markdown
# 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)
|