zeph-tui 0.20.1

Ratatui-based TUI dashboard with real-time metrics for Zeph
Documentation
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

/// Metadata for an active paste in the input buffer.
///
/// Present only while the input contains unsubmitted pasted text that was
/// multiline (two or more lines). Single-line pastes do not set this.
///
/// # Examples
///
/// ```rust
/// use zeph_tui::PasteState;
///
/// let ps = PasteState { line_count: 5, byte_len: 128 };
/// assert_eq!(ps.line_count, 5);
/// ```
#[derive(Debug, Clone)]
pub struct PasteState {
    /// Number of lines in the pasted text (always >= 2).
    pub line_count: usize,
    /// Byte length of the pasted text.
    pub byte_len: usize,
}

/// The current text-input mode of the TUI.
///
/// Inspired by modal editors: in `Normal` mode key bindings trigger actions;
/// in `Insert` mode printable characters are appended to the input buffer.
///
/// # Examples
///
/// ```rust
/// use zeph_tui::InputMode;
///
/// let mode = InputMode::Insert;
/// assert_eq!(mode, InputMode::Insert);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputMode {
    /// Navigation and command keybindings are active; typing does not insert text.
    Normal,
    /// Text is inserted into the input field on every printable key press.
    Insert,
}

/// The role of a message displayed in the chat widget.
///
/// The role controls the display style (colour, prefix label) applied by the
/// chat renderer.
///
/// # Examples
///
/// ```rust
/// use zeph_tui::MessageRole;
///
/// let role = MessageRole::User;
/// assert_eq!(role, MessageRole::User);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageRole {
    /// A message sent by the human user.
    User,
    /// A message generated by the AI assistant.
    Assistant,
    /// An internal system or meta message (e.g. session start notice).
    System,
    /// Output from a tool call execution.
    Tool,
}

/// A single entry in the TUI chat history buffer.
///
/// Carries the rendered text, role metadata, optional tool context, an inline
/// diff, and a wall-clock timestamp for display.
///
/// # Examples
///
/// ```rust
/// use zeph_tui::{ChatMessage, MessageRole};
///
/// let msg = ChatMessage::new(MessageRole::User, "Hello, agent!");
/// assert_eq!(msg.role, MessageRole::User);
/// assert_eq!(msg.content, "Hello, agent!");
/// assert!(!msg.streaming);
/// ```
#[derive(Debug, Clone)]
pub struct ChatMessage {
    /// Role that determines rendering style.
    pub role: MessageRole,
    /// Rendered text content of the message.
    pub content: String,
    /// `true` while the message is still being streamed from the LLM.
    pub streaming: bool,
    /// Name of the tool that produced this message, if any.
    pub tool_name: Option<zeph_common::ToolName>,
    /// Inline diff attached to a tool-output message.
    pub diff_data: Option<zeph_core::DiffData>,
    /// Human-readable filter statistics (e.g. "kept 12/40 lines").
    pub filter_stats: Option<String>,
    /// 0-based line indices preserved by the output filter, used for
    /// highlighting in the diff widget.
    pub kept_lines: Option<Vec<usize>>,
    /// Wall-clock time formatted as `HH:MM` when the message was created.
    pub timestamp: String,
    /// Number of lines in the pasted content when this message was submitted
    /// from a paste. `Some(n)` (n >= 2) enables collapsible display in the
    /// chat renderer; `None` means normal display.
    pub paste_line_count: Option<usize>,
}

impl ChatMessage {
    /// Create a new non-streaming message with the current local time as timestamp.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zeph_tui::{ChatMessage, MessageRole};
    ///
    /// let msg = ChatMessage::new(MessageRole::Assistant, "Done.");
    /// assert_eq!(msg.role, MessageRole::Assistant);
    /// assert!(!msg.streaming);
    /// ```
    pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
        Self {
            role,
            content: content.into(),
            streaming: false,
            tool_name: None,
            diff_data: None,
            filter_stats: None,
            kept_lines: None,
            timestamp: format_local_time(),
            paste_line_count: None,
        }
    }

    /// Mark this message as actively streaming.
    ///
    /// The chat widget renders a blinking cursor after the content while
    /// `streaming` is `true`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zeph_tui::{ChatMessage, MessageRole};
    ///
    /// let msg = ChatMessage::new(MessageRole::Assistant, "").streaming();
    /// assert!(msg.streaming);
    /// ```
    #[must_use]
    pub fn streaming(mut self) -> Self {
        self.streaming = true;
        self
    }

    /// Attach a tool name to this message for display in the chat header.
    ///
    /// # Examples
    ///
    /// ```rust
    /// use zeph_tui::{ChatMessage, MessageRole};
    ///
    /// let msg = ChatMessage::new(MessageRole::Tool, "output")
    ///     .with_tool(zeph_common::ToolName::new("bash"));
    /// assert!(msg.tool_name.is_some());
    /// ```
    #[must_use]
    pub fn with_tool(mut self, name: zeph_common::ToolName) -> Self {
        self.tool_name = Some(name);
        self
    }
}

fn format_local_time() -> String {
    chrono::Local::now().format("%H:%M").to_string()
}