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}