- fix(view): token streaming kini benar-benar tampil — sebelumnya cache display_lines tidak pernah di-rebuild saat pesan terakhir berubah (msg_count == cached_count), jadi teks AI streaming tidak pernah muncul sampai pesan baru/resize - perf(view): streaming kini hanya re-render pesan TERAKHIR (splice di batas cached_last_start) → O(konten baru) per token, bukan O(seluruh history); guard cached_last_len mencegah re-render pada frame spinner tanpa token baru - perf(run): skip chrono::Utc::now() + drain toasts saat tidak ada toast - perf(misc): drain_expired_toasts tidak lagi clone seluruh daftar toast - perf(view): render_toasts fast-path saat toasts kosong
193 lines
5.6 KiB
Rust
193 lines
5.6 KiB
Rust
//! Miscellaneous state: overlay enum, misc state bag, editor state.
|
|
|
|
use std::path::PathBuf;
|
|
use zesdex_infrastructure::Toast;
|
|
|
|
/// Which modal overlay, if any, is currently shown over the main TUI view.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Overlay {
|
|
/// No overlay; the main chat view is shown.
|
|
None,
|
|
/// Key bindings help screen.
|
|
Help,
|
|
/// Settings/configuration panel.
|
|
Settings,
|
|
/// Background bash job viewer.
|
|
Bash,
|
|
/// "Are you sure you want to quit?" confirmation.
|
|
QuitConfirm,
|
|
/// Raw key-code input capture (for binding custom keys).
|
|
KeyInput,
|
|
/// Inline editor (opened via `/edit`).
|
|
Editor,
|
|
/// Reasoning effort level selector.
|
|
Effort,
|
|
/// MCP server management panel.
|
|
Mcp,
|
|
/// Task list overlay.
|
|
Todo,
|
|
/// Project plan overlay.
|
|
Plan,
|
|
/// Session rewind / history scrubber.
|
|
Rewind,
|
|
/// Learning / lesson management panel.
|
|
Learning,
|
|
/// Token usage statistics panel.
|
|
Usage,
|
|
/// Generic loading spinner overlay.
|
|
Loading,
|
|
/// Model selector dropdown.
|
|
ModelSelector,
|
|
/// "Clear conversation?" confirmation.
|
|
ClearConfirm,
|
|
/// Git diff preview overlay.
|
|
Diff,
|
|
}
|
|
|
|
impl Overlay {
|
|
/// Human-readable name for this overlay variant.
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Overlay::None => "none",
|
|
Overlay::Help => "help",
|
|
Overlay::Settings => "settings",
|
|
Overlay::Bash => "bash",
|
|
Overlay::QuitConfirm => "quit_confirm",
|
|
Overlay::KeyInput => "key_input",
|
|
Overlay::Editor => "editor",
|
|
Overlay::Effort => "effort",
|
|
Overlay::Mcp => "mcp",
|
|
Overlay::Todo => "todo",
|
|
Overlay::Plan => "plan",
|
|
Overlay::Rewind => "rewind",
|
|
Overlay::Learning => "learning",
|
|
Overlay::Usage => "usage",
|
|
Overlay::Loading => "loading",
|
|
Overlay::ModelSelector => "model_selector",
|
|
Overlay::ClearConfirm => "clear_confirm",
|
|
Overlay::Diff => "diff",
|
|
}
|
|
}
|
|
|
|
/// Whether any overlay (i.e. anything other than `None`) is active.
|
|
pub fn is_active(self) -> bool {
|
|
!matches!(self, Overlay::None)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Overlay {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
/// Simple inline editor state for the TUI.
|
|
#[derive(Debug, Clone)]
|
|
pub struct EditorState {
|
|
/// Path to the file being edited.
|
|
pub path: PathBuf,
|
|
/// Current buffer content.
|
|
pub content: String,
|
|
/// Cursor position (byte offset).
|
|
pub cursor: usize,
|
|
}
|
|
|
|
impl EditorState {
|
|
/// Create a new editor state for the given path.
|
|
pub fn new(path: PathBuf, content: String) -> Self {
|
|
let cursor = content.len();
|
|
EditorState {
|
|
path,
|
|
content,
|
|
cursor,
|
|
}
|
|
}
|
|
|
|
/// Return the full buffer content.
|
|
pub fn as_string(&self) -> String {
|
|
self.content.clone()
|
|
}
|
|
|
|
/// Delete one character to the left of the cursor.
|
|
pub fn delete_left(&mut self) {
|
|
if self.cursor > 0 {
|
|
self.cursor -= 1;
|
|
self.content.remove(self.cursor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The "miscellaneous" slice of app state.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MiscState {
|
|
/// Currently active modal overlay (None = main chat view).
|
|
pub overlay: Overlay,
|
|
/// Active toast notifications.
|
|
pub toasts: Vec<Toast>,
|
|
/// Timestamp (ms) of the last staleness sweep for lesson cache.
|
|
pub last_staleness_sweep_ms: i64,
|
|
/// Whether the agent is currently "thinking".
|
|
pub thinking: bool,
|
|
/// Current LLM reasoning effort level (1-5).
|
|
pub effort_level: usize,
|
|
/// Currently focused index in list-type overlays.
|
|
pub selected_index: usize,
|
|
/// Optional inline editor state.
|
|
pub editor: Option<EditorState>,
|
|
/// Whether the API connection is established.
|
|
pub api_connected: bool,
|
|
/// Monotonically increasing tick count, incremented each render frame.
|
|
pub tick_count: u64,
|
|
/// Cached content of the TODO file.
|
|
pub todo_content: String,
|
|
/// Cached content of the PLAN file.
|
|
pub plan_content: String,
|
|
/// Whether a lesson background task is currently running.
|
|
pub lesson_running: bool,
|
|
/// Text waiting to be written to the system clipboard.
|
|
pub pending_clipboard_copy: Option<String>,
|
|
/// Cached git diff content for the preview overlay.
|
|
pub diff_content: String,
|
|
/// Scroll offset for the diff overlay.
|
|
pub diff_scroll: usize,
|
|
}
|
|
|
|
impl MiscState {
|
|
/// Create a fresh `MiscState` with no overlay, no toasts.
|
|
pub fn new() -> Self {
|
|
MiscState {
|
|
overlay: Overlay::None,
|
|
toasts: Vec::new(),
|
|
last_staleness_sweep_ms: 0,
|
|
thinking: false,
|
|
effort_level: 1,
|
|
selected_index: 0,
|
|
editor: None,
|
|
api_connected: false,
|
|
tick_count: 0,
|
|
todo_content: String::new(),
|
|
plan_content: String::new(),
|
|
lesson_running: false,
|
|
pending_clipboard_copy: None,
|
|
diff_content: String::new(),
|
|
diff_scroll: 0,
|
|
}
|
|
}
|
|
|
|
/// Append a toast notification to the active list.
|
|
pub fn push_toast(&mut self, toast: Toast) {
|
|
self.toasts.push(toast);
|
|
}
|
|
|
|
/// Remove all toasts whose lifetime has expired at `now_ms`.
|
|
pub fn drain_expired_toasts(&mut self, now_ms: i64) {
|
|
self.toasts.retain(|t| !t.expired(now_ms));
|
|
}
|
|
}
|
|
|
|
impl Default for MiscState {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|