1#[derive(Debug, Clone)]
5pub struct DisplayMessage {
6 pub role: DisplayRole,
7 pub content: String,
8 pub tool_call: Option<DisplayToolCall>,
10 pub collapsed: bool,
12 pub thinking_started_at: Option<std::time::Instant>,
14 pub thinking_duration_secs: Option<u64>,
16}
17
18impl DisplayMessage {
19 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 Interrupt,
39 Reasoning,
41 SlashCommand,
43 CommandResult,
45 Plan,
47}
48
49pub struct RoleStyle {
51 pub icon: String,
53 pub icon_style: ratatui::style::Style,
55 pub text_color: ratatui::style::Color,
57 pub continuation: &'static str,
59 pub attach_to_previous: bool,
61}
62
63impl DisplayRole {
64 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#[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 pub collapsed: bool,
120 pub result_lines: Vec<String>,
122 pub nested_calls: Vec<DisplayToolCall>,
124}
125
126impl DisplayToolCall {
127 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#[derive(Debug, Clone)]
173pub enum PendingItem {
174 UserMessage(String),
176 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#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum ToolState {
190 Pending,
192 Running,
194 Completed,
196 Error,
198 Cancelled,
200}
201
202impl ToolState {
203 pub fn is_finished(&self) -> bool {
205 matches!(self, Self::Completed | Self::Error | Self::Cancelled)
206 }
207
208 pub fn is_success(&self) -> bool {
210 matches!(self, Self::Completed)
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct ToolExecution {
217 pub id: String,
218 pub name: String,
219 pub output_lines: Vec<String>,
220 pub state: ToolState,
222 pub elapsed_secs: u64,
224 pub started_at: std::time::Instant,
226 pub tick_count: usize,
228 pub parent_id: Option<String>,
230 pub depth: usize,
232 pub args: std::collections::HashMap<String, serde_json::Value>,
234}
235
236impl ToolExecution {
237 pub fn is_finished(&self) -> bool {
239 self.state.is_finished()
240 }
241
242 pub fn is_success(&self) -> bool {
244 self.state.is_success()
245 }
246}
247
248#[cfg(test)]
249#[path = "types_tests.rs"]
250mod tests;