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}
120
121impl ChatMessage {
122    /// Create a new non-streaming message with the current local time as timestamp.
123    ///
124    /// # Examples
125    ///
126    /// ```rust
127    /// use zeph_tui::{ChatMessage, MessageRole};
128    ///
129    /// let msg = ChatMessage::new(MessageRole::Assistant, "Done.");
130    /// assert_eq!(msg.role, MessageRole::Assistant);
131    /// assert!(!msg.streaming);
132    /// ```
133    pub fn new(role: MessageRole, content: impl Into<String>) -> Self {
134        Self {
135            role,
136            content: content.into(),
137            streaming: false,
138            tool_name: None,
139            diff_data: None,
140            filter_stats: None,
141            kept_lines: None,
142            timestamp: format_local_time(),
143            paste_line_count: None,
144            tool_call_id: None,
145            success: None,
146        }
147    }
148
149    /// Mark this message as actively streaming.
150    ///
151    /// The chat widget renders a blinking cursor after the content while
152    /// `streaming` is `true`.
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// use zeph_tui::{ChatMessage, MessageRole};
158    ///
159    /// let msg = ChatMessage::new(MessageRole::Assistant, "").streaming();
160    /// assert!(msg.streaming);
161    /// ```
162    #[must_use]
163    pub fn streaming(mut self) -> Self {
164        self.streaming = true;
165        self
166    }
167
168    /// Attach a tool name to this message for display in the chat header.
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use zeph_tui::{ChatMessage, MessageRole};
174    ///
175    /// let msg = ChatMessage::new(MessageRole::Tool, "output")
176    ///     .with_tool(zeph_common::ToolName::new("bash"));
177    /// assert!(msg.tool_name.is_some());
178    /// ```
179    #[must_use]
180    pub fn with_tool(mut self, name: zeph_common::ToolName) -> Self {
181        self.tool_name = Some(name);
182        self
183    }
184
185    /// Attach a `tool_call_id` for id-based event correlation.
186    ///
187    /// Used to correlate streaming [`crate::event::AgentEvent::ToolOutputChunk`] and
188    /// `DiffReady` events with the originating tool call when multiple tools execute concurrently.
189    ///
190    /// # Examples
191    ///
192    /// ```rust
193    /// use zeph_tui::{ChatMessage, MessageRole};
194    ///
195    /// let msg = ChatMessage::new(MessageRole::Tool, "")
196    ///     .with_tool_call_id("call-abc-123".to_owned());
197    /// assert_eq!(msg.tool_call_id.as_deref(), Some("call-abc-123"));
198    /// ```
199    #[must_use]
200    pub fn with_tool_call_id(mut self, id: String) -> Self {
201        self.tool_call_id = Some(id);
202        self
203    }
204}
205
206fn format_local_time() -> String {
207    chrono::Local::now().format("%H:%M").to_string()
208}