167 lines
7.4 KiB
Markdown
167 lines
7.4 KiB
Markdown
# TUI (Terminal User Interface)
|
||
|
||
Dibangun di atas **ratatui** + **crossterm**. Kode ada di `apps/interfaces/tui/src/`.
|
||
|
||
## Struktur Source
|
||
|
||
```
|
||
apps/interfaces/tui/src/
|
||
├── run.rs # Event loop utama
|
||
├── state.rs # AppStateRest — single source of truth
|
||
├── action.rs # apply_action(): satu-satunya mutator state
|
||
├── turn.rs # Spawn agent turn di background thread
|
||
├── lib.rs # Re-export publik
|
||
├── controller/
|
||
│ ├── input.rs # Key handler → Vec<Action>
|
||
│ └── command.rs # Slash command parser
|
||
├── view/
|
||
│ ├── mod.rs # Layout + pre_render() + draw()
|
||
│ ├── chat.rs # Chat transcript panel (dengan display cache)
|
||
│ ├── sidebar.rs # Sidebar: workflow, tasks, usage
|
||
│ ├── status.rs # Status bar satu baris
|
||
│ ├── markdown.rs # Markdown → styled Span (pulldown-cmark)
|
||
│ ├── workflow.rs # Workflow/hive-mind progress panel
|
||
│ ├── theme.rs # Tokyo Night color palette (const)
|
||
│ └── overlays/ # 16 overlay panel
|
||
└── model/ # Data model lokal TUI
|
||
```
|
||
|
||
## Render Pipeline (Per Frame)
|
||
|
||
```
|
||
run_loop_inner() [50ms in-flight / 200ms idle]
|
||
│
|
||
├── drain expired toasts (1x, bukan 2x)
|
||
│
|
||
├── if dirty:
|
||
│ view::pre_render(&mut state) ← update cache (markdown, token count)
|
||
│ terminal.draw(|f| view::draw(f, &state))
|
||
│ state.dirty = false
|
||
│
|
||
└── poll events → apply_action → Action::Tick
|
||
```
|
||
|
||
### Optimasi Performa
|
||
|
||
| Masalah lama | Solusi saat ini |
|
||
|---|---|
|
||
| `count_tokens` (tiktoken) setiap frame | Cache `cached_token_count`, update hanya saat pesan baru |
|
||
| `render_markdown` ulang setiap frame | `display_lines_cache` di `AppStateRest`, rebuild saat `transcript_cache.dirty` |
|
||
| `Vec::remove(0)` untuk evict pesan lama | `VecDeque::pop_front()` — O(1) |
|
||
| `Mutex<bool>` untuk `turn_in_flight` | `Arc<AtomicBool>` — lock-free |
|
||
| Render terus meski idle | Skip `terminal.draw()` jika `dirty == false` |
|
||
| Poll 50ms konstan | Adaptif: 50ms saat in-flight, 200ms saat idle |
|
||
| `drain_expired_toasts` 2x per iterasi | Sekali saja di `run_loop_inner` |
|
||
|
||
## State (AppStateRest)
|
||
|
||
`AppStateRest` di `state.rs` adalah satu-satunya sumber kebenaran TUI:
|
||
|
||
```
|
||
AppStateRest {
|
||
settings: Settings // provider, model, dll
|
||
app_config: AppConfig // endpoint, env vars
|
||
workspace_roots: Vec<PathBuf> // working directories
|
||
session_dir / session_id // path sesi aktif
|
||
memory_dir // direktori memory
|
||
session_runtime: Option<SessionRuntime> // history pesan, usage stats
|
||
|
||
transcript_cache: TranscriptCache // VecDeque<ChatMessageDisplay>
|
||
scroll: ScrollState // offset scroll pane chat
|
||
input: InputState // buffer, cursor, history, autocomplete
|
||
misc: MiscState // overlay aktif, toasts, flags
|
||
|
||
turn_events: Arc<Mutex<VecDeque<TurnEvent>>> // queue event dari agent
|
||
turn_in_flight_flag: Arc<AtomicBool> // apakah agent sedang jalan
|
||
abort_flag: Arc<AtomicBool> // sinyal abort oleh user
|
||
|
||
// Cache performa
|
||
display_lines_cache: Vec<Line<'static>> // hasil render markdown
|
||
cached_token_count: usize // token count terkini
|
||
token_count_dirty: bool // perlu hitung ulang?
|
||
last_render_width: u16 // lebar terminal saat render terakhir
|
||
|
||
dirty: bool // perlu render ulang?
|
||
quit: bool // keluar dari loop?
|
||
}
|
||
```
|
||
|
||
**Aturan mutasi:**
|
||
- Dimutasi hanya dari `action.rs::apply_action()` dan `run.rs` (untuk dirty/quit)
|
||
- Semua fungsi `view/*` bersifat read-only terhadap state
|
||
- `pre_render_chat()` boleh mutasi hanya field cache (`display_lines_cache`, `cached_token_count`, `token_count_dirty`)
|
||
|
||
## Input & Actions
|
||
|
||
`controller/input.rs::handle_key()` → `Vec<Action>` → `apply_action(&mut state, action)`
|
||
|
||
Semua mutasi state melewati satu titik: `apply_action`. Controller tidak tahu *bagaimana* state diubah, hanya *action apa* yang dihasilkan.
|
||
|
||
### Action Utama
|
||
|
||
| Action | Efek |
|
||
|--------|------|
|
||
| `SubmitInput(text)` | Push ke transcript, spawn agent turn |
|
||
| `Tick` | Drain `TurnEvent` queue, update state dari hasil agent |
|
||
| `ScrollUp/Down` | Ubah `scroll.offset` |
|
||
| `OpenOverlay(v)` | Set `misc.overlay = v` |
|
||
| `Resize(w, h)` | Invalidasi cache display, set `last_render_width` |
|
||
| `AbortTurn` | Store `true` ke `abort_flag` |
|
||
| `ForceQuit` | Set `quit = true` |
|
||
|
||
## Overlays (16 Panel)
|
||
|
||
| Overlay | File | Fungsi |
|
||
|---------|------|--------|
|
||
| `Help` | `overlays/help.rs` | Daftar shortcut keyboard |
|
||
| `Settings` | `overlays/settings.rs` | Panel pengaturan |
|
||
| `Bash` | `overlays/bash.rs` | Background shell jobs |
|
||
| `QuitConfirm` | `overlays/quit_confirm.rs` | Konfirmasi keluar |
|
||
| `KeyInput` | `overlays/key_input.rs` | Capture key binding |
|
||
| `Editor` | `overlays/editor.rs` | File editor inline |
|
||
| `Effort` | `overlays/effort.rs` | Pilih level reasoning LLM |
|
||
| `Mcp` | `overlays/mcp.rs` | Manajemen MCP server |
|
||
| `Todo` | `overlays/todo.rs` | Daftar TODO |
|
||
| `Rewind` | `overlays/rewind.rs` | Navigasi history pesan |
|
||
| `Learning` | `overlays/learning.rs` | Viewer lesson |
|
||
| `Usage` | `overlays/usage.rs` | Statistik token |
|
||
| `Loading` | `overlays/loading.rs` | Spinner generik |
|
||
| `ModelSelector` | `overlays/model_selector.rs` | Pilih model LLM |
|
||
| `ClearConfirm` | `overlays/clear_confirm.rs` | Konfirmasi clear chat |
|
||
|
||
## Layout Terminal
|
||
|
||
```
|
||
┌───────────────────────────────────────────────┐
|
||
│ │
|
||
│ Chat Transcript Sidebar (≥90) │
|
||
│ (view/chat.rs) ┌────────────┐ │
|
||
│ VecDeque messages │ Workflow │ │
|
||
│ + markdown cache ├────────────┤ │
|
||
│ scrollable │ Tasks │ │
|
||
│ ├────────────┤ │
|
||
│ │ Usage │ │
|
||
│ └────────────┘ │
|
||
├───────────────────────────────────────────────┤
|
||
│ ❯ Input Bar + Autocomplete dropdown │
|
||
├───────────────────────────────────────────────┤
|
||
│ ⚡zesdex READY │ ...center... │ tok · model │
|
||
└───────────────────────────────────────────────┘
|
||
```
|
||
|
||
Sidebar hanya tampil jika lebar terminal ≥ 90 kolom.
|
||
|
||
## Theme
|
||
|
||
`view/theme.rs` mendefinisikan palette **Tokyo Night** sebagai `const Color`:
|
||
`PRIMARY`, `BG`, `SURFACE`, `SURFACE_ELEVATED`, `BORDER`, `TEXT`, `TEXT_DIM`, `TEXT_MUTED`, `SUCCESS`, `WARNING`, `ERROR`, `INFO`, `HIGHLIGHT`, `CODE_BG`, dll.
|
||
|
||
## Markdown Rendering
|
||
|
||
`view/markdown.rs::render_markdown(text, width, dim)`:
|
||
- Parse dengan `pulldown-cmark`
|
||
- Hasilkan `Vec<Span<'static>>` dengan styling
|
||
- Support: heading, code block, diff block (warna +/-/@@), list, blockquote, table, inline code, link
|
||
- `dim=true` → semua span memakai `TEXT_DIM` + italic (untuk tool output)
|
||
- Hasil di-cache di `AppStateRest::display_lines_cache`
|