Skip to main content

zeph_tui/
types.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4/// Metadata for an active paste in the input buffer.
5///
6/// Present only while the input contains unsubmitted pasted text that was
7/// multiline (two or more lines). Single-line pastes do not set this.
8///
9/// # Examples
10///
11/// ```rust
12/// use zeph_tui::PasteState;
13///
14/// let ps = PasteState { line_count: 5, byte_len: 128 };
15/// assert_eq!(ps.line_count, 5);
16/// ```
17#[derive(Debug, Clone)]
18pub struct PasteState {
19    /// Number of lines in the pasted text (always >= 2).
20    pub line_count: usize,
21    /// Byte length of the pasted text.
22    pub byte_len: usize,
23}
24
25/// The current text-input mode of the TUI.
26///
27/// Inspired by modal editors: in `Normal` mode key bindings trigger actions;
28/// in `Insert` mode printable characters are appended to the input buffer.
29///
30/// # Examples
31///
32/// ```rust
33/// use zeph_tui::InputMode;
34///
35/// let mode = InputMode::Insert;
36/// assert_eq!(mode, InputMode::Insert);
37/// ```
38#[non_exhaustive]
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum InputMode {
41    /// Navigation and command keybindings are active; typing does not insert text.
42    Normal,
43    /// Text is inserted into the input field on every printable key press.
44    Insert,
45}
46
47/// The role of a message displayed in the chat widget.
48///
49/// The role controls the display style (colour, prefix label) applied by the
50/// chat renderer.
51///
52/// # Examples
53///
54/// ```rust
55/// use zeph_tui::MessageRole;
56///
57/// let role = MessageRole::User;
58/// assert_eq!(role, MessageRole::User);
59/// ```
60#[non_exhaustive]
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum MessageRole {
63    /// A message sent by the human user.
64    User,
65    /// A message generated by the AI assistant.
66    Assistant,
67    /// An internal system or meta message (e.g. session start notice).
68    System,
69    /// Output from a tool call execution.
70    Tool,
71}
72
73/// A single entry in the TUI chat history buffer.
74///
75/// Carries the rendered text, role metadata, optional tool context, an inline
76/// diff, and a wall-clock timestamp for display.
77///
78/// # Examples
79///
80/// ```rust
81/// use zeph_tui::{ChatMessage, MessageRole};
82///
83/// let msg = ChatMessage::new(MessageRole::User, "Hello, agent!");
84/// assert_eq!(msg.role, MessageRole::User);
85/// assert_eq!(msg.content, "Hello, agent!");
86/// assert!(!msg.streaming);
87/// ```
88#[derive(Debug, Clone)]
89pub struct ChatMessage {
90    /// Role that determines rendering style.
91    pub role: MessageRole,
92    /// Rendered text content of the message.
93    pub content: String,
94    /// `true` while the message is still being streamed from the LLM.
95    pub streaming: bool,
96    /// Name of the tool that produced this message, if any.
97    pub tool_name: Option<zeph_common::ToolName>,
98    /// Inline diff attached to a tool-output message.
99    pub diff_data: Option<zeph_core::DiffData>,
100    /// Human-readable filter statistics (e.g. "kept 12/40 lines").
101    pub filter_stats: Option<String>,
102    /// 0-based line indices preserved by the output filter, used for
103    /// highlighting in the diff widget.
104    pub kept_lines: Option<Vec<usize>>,
105    /// Wall-clock time formatted as `HH:MM` when the message was created.
106    pub timestamp: String,
107    /// Number of lines in the pasted content when this message was submitted
108    /// from a paste. `Some(n)` (n >= 2) enables collapsible display in the
109    /// chat renderer; `None` means normal display.
110    pub paste_line_count: Option<usize>,
111    /// Opaque tool-call identifier forwarded from the agent loop.
112    ///
113    /// Used to correlate `DiffReady` and `ToolOutputChunk` events with the
114    /// correct `ChatMessage` when multiple tools run concurrently.
115    pub tool_call_id: Option<String>,
116    /// Whether the tool call succeeded. `None` while streaming, `Some(true)` on
117    /// success, `Some(false)` on error.
118    pub success: Option<bool>,
119    /// Whether this tool call originates from an MCP server rather than a native tool.
120    pub is_mcp: bool,
121}
122
123impl ChatMessage {
124    /// Create a new non-streaming message with the current local time as timestamp.
125    ///
126    /// # Examples
127    ///
128    /// ```rust
129    /// use zeph_tui::{ChatMessage, MessageRole};
130    ///
131    /// let msg = ChatMessage::new(MessageRole::Assistant, "Done.");
132    /// assert_eq!(msg.role, MessageRole::Assistant);
133    /// assert!(!msg.streaming);
134    /// ```
135    pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
136        Self {
137            role,
138            content: content.into(),
139            streaming: false,
140            tool_name: None,
141            diff_data: None,
142            filter_stats: None,
143            kept_lines: None,
144            timestamp: format_local_time(),
145            paste_line_count: None,
146            tool_call_id: None,
147            success: None,
148            is_mcp: false,
149        }
150    }
151
152    /// Mark this message as actively streaming.
153    ///
154    /// The chat widget renders a blinking cursor after the content while
155    /// `streaming` is `true`.
156    ///
157    /// # Examples
158    ///
159    /// ```rust
160    /// use zeph_tui::{ChatMessage, MessageRole};
161    ///
162    /// let msg = ChatMessage::new(MessageRole::Assistant, "").streaming();
163    /// assert!(msg.streaming);
164    /// ```
165    #[must_use]
166    pub fn streaming(mut self) -> Self {
167        self.streaming = true;
168        self
169    }
170
171    /// Attach a tool name to this message for display in the chat header.
172    ///
173    /// # Examples
174    ///
175    /// ```rust
176    /// use zeph_tui::{ChatMessage, MessageRole};
177    ///
178    /// let msg = ChatMessage::new(MessageRole::Tool, "output")
179    ///     .with_tool(zeph_common::ToolName::new("bash"));
180    /// assert!(msg.tool_name.is_some());
181    /// ```
182    #[must_use]
183    pub fn with_tool(mut self, name: zeph_common::ToolName) -> Self {
184        self.tool_name = Some(name);
185        self
186    }
187
188    /// Attach a `tool_call_id` for id-based event correlation.
189    ///
190    /// Used to correlate streaming [`crate::event::AgentEvent::ToolOutputChunk`] and
191    /// `DiffReady` events with the originating tool call when multiple tools execute concurrently.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use zeph_tui::{ChatMessage, MessageRole};
197    ///
198    /// let msg = ChatMessage::new(MessageRole::Tool, "")
199    ///     .with_tool_call_id("call-abc-123".to_owned());
200    /// assert_eq!(msg.tool_call_id.as_deref(), Some("call-abc-123"));
201    /// ```
202    #[must_use]
203    pub fn with_tool_call_id(mut self, id: String) -> Self {
204        self.tool_call_id = Some(id);
205        self
206    }
207
208    /// Mark whether this tool call originates from an MCP server.
209    ///
210    /// Used by the chat widget to classify the message's `ToolKind` for icon/color
211    /// selection.
212    ///
213    /// # Examples
214    ///
215    /// ```rust
216    /// use zeph_tui::{ChatMessage, MessageRole};
217    ///
218    /// let msg = ChatMessage::new(MessageRole::Tool, "").with_is_mcp(true);
219    /// assert!(msg.is_mcp);
220    /// ```
221    #[must_use]
222    pub fn with_is_mcp(mut self, is_mcp: bool) -> Self {
223        self.is_mcp = is_mcp;
224        self
225    }
226}
227
228fn format_local_time() -> String {
229    chrono::Local::now().format("%H:%M").to_string()
230}