# 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 │ └── 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` untuk `turn_in_flight` | `Arc` — 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 // working directories session_dir / session_id // path sesi aktif memory_dir // direktori memory session_runtime: Option // history pesan, usage stats transcript_cache: TranscriptCache // VecDeque scroll: ScrollState // offset scroll pane chat input: InputState // buffer, cursor, history, autocomplete misc: MiscState // overlay aktif, toasts, flags turn_events: Arc>> // queue event dari agent turn_in_flight_flag: Arc // apakah agent sedang jalan abort_flag: Arc // sinyal abort oleh user // Cache performa display_lines_cache: Vec> // 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` → `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>` 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`