Skip to main content

opendev_tui/app/
types.rs

1//! Display types for the TUI conversation view.
2
3/// A message prepared for display in the conversation widget.
4#[derive(Debug, Clone)]
5pub struct DisplayMessage {
6    pub role: DisplayRole,
7    pub content: String,
8    /// Optional tool call info for assistant messages.
9    pub tool_call: Option<DisplayToolCall>,
10    /// Whether this message is collapsed.
11    pub collapsed: bool,
12    /// When reasoning started (for computing finalized duration).
13    pub thinking_started_at: Option<std::time::Instant>,
14    /// Finalized thinking duration in seconds (set when thinking ends).
15    pub thinking_duration_secs: Option<u64>,
16}
17
18impl DisplayMessage {
19    /// Convenience constructor for the common case (no tool call, not collapsed).
20    pub fn new(role: DisplayRole, content: impl Into<String>) -> Self {
21        Self {
22            role,
23            content: content.into(),
24            tool_call: None,
25            collapsed: false,
26            thinking_started_at: None,
27            thinking_duration_secs: None,
28        }
29    }
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum DisplayRole {
34    User,
35    Assistant,
36    System,
37    /// Interrupted feedback — rendered with ⎿ in red.
38    Interrupt,
39    /// Native reasoning content from the LLM (inline thinking).
40    Reasoning,
41    /// Slash command echo — rendered with `❯ ` prefix in accent+bold.
42    SlashCommand,
43    /// Slash command result — rendered with `  ⎿  ` prefix, attaches to previous.
44    CommandResult,
45    /// Plan content — rendered in a bordered panel with markdown.
46    Plan,
47}
48
49/// Rendering configuration for simple (non-markdown, non-collapsible) roles.
50pub struct RoleStyle {
51    /// Prefix string for the first line (e.g. "> ", "! ", "  ⎿  ")
52    pub icon: String,
53    /// Style for the icon span
54    pub icon_style: ratatui::style::Style,
55    /// Color for the content text
56    pub text_color: ratatui::style::Color,
57    /// Continuation prefix for wrapped lines (must match icon visual width)
58    pub continuation: &'static str,
59    /// Whether to suppress the blank line before this message
60    pub attach_to_previous: bool,
61}
62
63impl DisplayRole {
64    /// Returns a `RoleStyle` for roles that use the standard icon+text pattern.
65    /// Returns `None` for Assistant (it has custom rendering).
66    pub fn style(&self) -> Option<RoleStyle> {
67        use crate::formatters::style_tokens::{self, Indent};
68        use crate::widgets::spinner::CONTINUATION_CHAR;
69        use ratatui::style::{Modifier, Style};
70
71        match self {
72            Self::User => Some(RoleStyle {
73                icon: "> ".to_string(),
74                icon_style: Style::default()
75                    .fg(style_tokens::ACCENT)
76                    .add_modifier(Modifier::BOLD),
77                text_color: style_tokens::PRIMARY,
78                continuation: Indent::CONT,
79                attach_to_previous: false,
80            }),
81            Self::Interrupt => Some(RoleStyle {
82                icon: format!("  {CONTINUATION_CHAR}  "),
83                icon_style: Style::default()
84                    .fg(style_tokens::ERROR)
85                    .add_modifier(Modifier::BOLD),
86                text_color: style_tokens::ERROR,
87                continuation: Indent::RESULT_CONT,
88                attach_to_previous: true,
89            }),
90            Self::SlashCommand => Some(RoleStyle {
91                icon: "❯ ".to_string(),
92                icon_style: Style::default()
93                    .fg(style_tokens::ACCENT)
94                    .add_modifier(Modifier::BOLD),
95                text_color: style_tokens::PRIMARY,
96                continuation: Indent::CONT,
97                attach_to_previous: false,
98            }),
99            Self::CommandResult => Some(RoleStyle {
100                icon: format!("  {CONTINUATION_CHAR}  "),
101                icon_style: Style::default().fg(style_tokens::ACCENT),
102                text_color: style_tokens::SUBTLE,
103                continuation: Indent::RESULT_CONT,
104                attach_to_previous: true,
105            }),
106            Self::Assistant | Self::System | Self::Reasoning | Self::Plan => None,
107        }
108    }
109}
110
111/// Tool call display info.
112#[derive(Debug, Clone)]
113pub struct DisplayToolCall {
114    pub name: String,
115    pub arguments: std::collections::HashMap<String, serde_json::Value>,
116    pub summary: Option<String>,
117    pub success: bool,
118    /// Whether this tool result is collapsed (user can toggle).
119    pub collapsed: bool,
120    /// Result lines for expanded view.
121    pub result_lines: Vec<String>,
122    /// Nested tool calls (from subagent execution).
123    pub nested_calls: Vec<DisplayToolCall>,
124}
125
126impl DisplayToolCall {
127    /// Convert a model `ToolCall` into a `DisplayToolCall` with smart collapse
128    /// and result extraction.  Used by both history hydration and the batch
129    /// message handler so they produce identical output.
130    pub fn from_model(tc: &opendev_models::message::ToolCall) -> Self {
131        use crate::formatters::tool_registry::{ToolCategory, categorize_tool};
132        use crate::widgets::conversation::is_diff_tool;
133
134        let result_lines: Vec<String> = tc
135            .result
136            .as_ref()
137            .map(|r| {
138                let text = match r {
139                    serde_json::Value::String(s) => s.clone(),
140                    other => serde_json::to_string_pretty(other).unwrap_or_default(),
141                };
142                text.lines().take(50).map(|l| l.to_string()).collect()
143            })
144            .unwrap_or_default();
145
146        let category = categorize_tool(&tc.name);
147        let is_file_read = category == ToolCategory::FileRead;
148        let is_bash = category == ToolCategory::Bash;
149        let collapsed = is_file_read
150            || (is_bash && result_lines.len() > 4)
151            || (!is_bash && result_lines.len() > 5 && !is_diff_tool(&tc.name));
152
153        let nested_calls = tc
154            .nested_tool_calls
155            .iter()
156            .map(DisplayToolCall::from_model)
157            .collect();
158
159        Self {
160            name: tc.name.clone(),
161            arguments: tc.parameters.clone(),
162            summary: tc.result_summary.clone(),
163            success: tc.error.is_none(),
164            collapsed,
165            result_lines,
166            nested_calls,
167        }
168    }
169}
170
171/// A queued item waiting to be processed by the foreground agent.
172#[derive(Debug, Clone)]
173pub enum PendingItem {
174    /// A user message typed while the agent was busy.
175    UserMessage(String),
176    /// A completed background agent result.
177    BackgroundResult {
178        task_id: String,
179        query: String,
180        result: String,
181        success: bool,
182        tool_call_count: usize,
183        cost_usd: f64,
184    },
185}
186
187/// State of a tool execution lifecycle.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum ToolState {
190    /// Tool is queued but not yet executing.
191    Pending,
192    /// Tool is currently executing.
193    Running,
194    /// Tool finished successfully.
195    Completed,
196    /// Tool finished with an error.
197    Error,
198    /// Tool was cancelled before completion.
199    Cancelled,
200}
201
202impl ToolState {
203    /// Returns true if the tool is in a terminal state (Completed, Error, or Cancelled).
204    pub fn is_finished(&self) -> bool {
205        matches!(self, Self::Completed | Self::Error | Self::Cancelled)
206    }
207
208    /// Returns true if the tool completed successfully.
209    pub fn is_success(&self) -> bool {
210        matches!(self, Self::Completed)
211    }
212}
213
214/// Active tool execution being displayed.
215#[derive(Debug, Clone)]
216pub struct ToolExecution {
217    pub id: String,
218    pub name: String,
219    pub output_lines: Vec<String>,
220    /// Current state of the tool execution.
221    pub state: ToolState,
222    /// Elapsed seconds since tool started.
223    pub elapsed_secs: u64,
224    /// Start timestamp for elapsed time calculation.
225    pub started_at: std::time::Instant,
226    /// Animation frame counter — incremented every tick for smooth spinner.
227    pub tick_count: usize,
228    /// Parent tool ID for nested tool calls.
229    pub parent_id: Option<String>,
230    /// Nesting depth (0 = top-level).
231    pub depth: usize,
232    /// Tool arguments for display.
233    pub args: std::collections::HashMap<String, serde_json::Value>,
234}
235
236impl ToolExecution {
237    /// Whether the tool execution has finished (terminal state).
238    pub fn is_finished(&self) -> bool {
239        self.state.is_finished()
240    }
241
242    /// Whether the tool execution was successful.
243    pub fn is_success(&self) -> bool {
244        self.state.is_success()
245    }
246}
247
248#[cfg(test)]
249#[path = "types_tests.rs"]
250mod tests;