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}