feat(tui): introduce comprehensive state management for TUI interface
- Add AppStateRest as the central state struct for managing TUI state. - Implement InputState for handling user input, autocomplete, and history. - Create MiscState to manage overlays, notifications, and editor state. - Introduce ScrollState for viewport scrolling functionality. - Develop TranscriptCache for efficient message rendering in the chat pane. - Implement SimpleAgent and SimpleWorkflowEngine for agent lifecycle management. - Add helper functions for managing effort levels and token counting. - Organize state-related modules for better maintainability and clarity.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
//! 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 and return all toasts whose lifetime has expired at `now_ms`.
|
||||
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<Toast> {
|
||||
let expired: Vec<_> = self
|
||||
.toasts
|
||||
.iter()
|
||||
.filter(|t| t.expired(now_ms))
|
||||
.cloned()
|
||||
.collect();
|
||||
self.toasts.retain(|t| !t.expired(now_ms));
|
||||
expired
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MiscState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user