2026-07-20 13:59:37 +07:00
|
|
|
|
# TUI (Terminal User Interface)
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
Dibangun di atas **ratatui** + **crossterm**. Kode ada di `apps/interfaces/tui/src/`.
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
## Struktur Source
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-07-20 13:59:37 +07:00
|
|
|
|
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
|
2026-07-16 07:42:03 +07:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
## Render Pipeline (Per Frame)
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-07-20 13:59:37 +07:00
|
|
|
|
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
|
2026-07-16 07:42:03 +07:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
### Optimasi Performa
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
| 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` |
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
## 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.
|
2026-07-16 07:42:03 +07:00
|
|
|
|
|
|
|
|
|
|
## Theme
|
|
|
|
|
|
|
2026-07-20 13:59:37 +07:00
|
|
|
|
`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`
|