steer_tui/tui/widgets/formatters/
view.rs

1use super::{ToolFormatter, helpers::*};
2use crate::tui::theme::{Component, Theme};
3use ratatui::{
4    style::{Modifier, Style},
5    text::{Line, Span},
6};
7use serde_json::Value;
8use std::path::Path;
9use steer_core::app::conversation::ToolResult;
10use steer_tools::tools::view::ViewParams;
11
12pub struct ViewFormatter;
13const MAX_LINES: usize = 100;
14
15impl ToolFormatter for ViewFormatter {
16    fn compact(
17        &self,
18        params: &Value,
19        result: &Option<ToolResult>,
20        _wrap_width: usize,
21        theme: &Theme,
22    ) -> Vec<Line<'static>> {
23        let mut lines = Vec::new();
24
25        let Ok(params) = serde_json::from_value::<ViewParams>(params.clone()) else {
26            return vec![Line::from(Span::styled(
27                "Invalid view params",
28                theme.style(Component::ErrorText),
29            ))];
30        };
31
32        let file_name = Path::new(&params.file_path)
33            .file_name()
34            .and_then(|s| s.to_str())
35            .unwrap_or(&params.file_path);
36
37        let mut spans = vec![Span::styled(file_name.to_string(), Style::default())];
38
39        // Add line range info if present
40        if params.offset.is_some() || params.limit.is_some() {
41            let offset = params.offset.map_or(1, |o| o + 1);
42            let limit = params.limit.unwrap_or(0);
43            let end_line = if limit > 0 { offset + limit - 1 } else { 0 };
44
45            if limit > 0 {
46                spans.push(Span::styled(
47                    format!(" [{offset}-{end_line}]"),
48                    Style::default(),
49                ));
50            } else {
51                spans.push(Span::styled(format!(" [{offset}+]"), Style::default()));
52            }
53        }
54
55        // Add count info from results
56        let info = extract_view_info(result);
57        spans.push(Span::raw(" "));
58        spans.push(Span::styled(
59            format!("({info})"),
60            theme
61                .style(Component::DimText)
62                .add_modifier(Modifier::ITALIC),
63        ));
64
65        lines.push(Line::from(spans));
66        lines
67    }
68
69    fn detailed(
70        &self,
71        params: &Value,
72        result: &Option<ToolResult>,
73        wrap_width: usize,
74        theme: &Theme,
75    ) -> Vec<Line<'static>> {
76        let mut lines = Vec::new();
77
78        let Ok(params) = serde_json::from_value::<ViewParams>(params.clone()) else {
79            return vec![Line::from(Span::styled(
80                "Invalid view params",
81                theme.style(Component::ErrorText),
82            ))];
83        };
84
85        // Show file path
86        lines.push(Line::from(Span::styled(
87            format!("File: {}", params.file_path),
88            theme
89                .style(Component::CodeFilePath)
90                .add_modifier(Modifier::BOLD),
91        )));
92
93        // Show line range if specified
94        if params.offset.is_some() || params.limit.is_some() {
95            lines.push(Line::from(Span::styled(
96                format!("Lines: {}", format_line_range(params.offset, params.limit)),
97                theme.style(Component::DimText),
98            )));
99        }
100
101        // Show output if we have results
102        if let Some(result) = result {
103            match result {
104                ToolResult::FileContent(file_content) => {
105                    if !file_content.content.is_empty() {
106                        lines.push(separator_line(wrap_width, theme.style(Component::DimText)));
107
108                        let (output_lines, truncated) =
109                            truncate_lines(&file_content.content, MAX_LINES);
110
111                        // Trim line number & tab
112                        let trimmed_lines: Vec<&str> = output_lines
113                            .iter()
114                            .map(|line| if line.len() > 6 { &line[6..] } else { "" })
115                            .collect();
116
117                        for line in trimmed_lines {
118                            for wrapped in textwrap::wrap(line, wrap_width) {
119                                lines.push(Line::from(Span::raw(wrapped.to_string())));
120                            }
121                        }
122
123                        if truncated {
124                            lines.push(Line::from(Span::styled(
125                                format!(
126                                    "... ({} more lines)",
127                                    file_content.content.lines().count() - MAX_LINES
128                                ),
129                                theme
130                                    .style(Component::DimText)
131                                    .add_modifier(Modifier::ITALIC),
132                            )));
133                        }
134                    } else {
135                        lines.push(Line::from(Span::styled(
136                            "(Empty file)",
137                            theme
138                                .style(Component::DimText)
139                                .add_modifier(Modifier::ITALIC),
140                        )));
141                    }
142                }
143                ToolResult::Error(error) => {
144                    lines.push(separator_line(wrap_width, theme.style(Component::DimText)));
145                    lines.push(Line::from(Span::styled(
146                        error.to_string(),
147                        theme.style(Component::ErrorText),
148                    )));
149                }
150                _ => {
151                    lines.push(Line::from(Span::styled(
152                        "Unexpected result type",
153                        theme.style(Component::ErrorText),
154                    )));
155                }
156            }
157        }
158
159        lines
160    }
161}
162
163fn extract_view_info(result: &Option<ToolResult>) -> String {
164    match result {
165        Some(ToolResult::FileContent(file_content)) => {
166            let line_count = file_content.content.lines().count();
167            format!("{line_count} lines")
168        }
169        Some(ToolResult::Error(_)) => "error".to_string(),
170        _ => "pending".to_string(),
171    }
172}
173
174fn format_line_range(offset: Option<u64>, limit: Option<u64>) -> String {
175    let start = offset.map_or(1, |o| o + 1);
176    match limit {
177        Some(l) if l > 0 => format!("{}-{}", start, start + l - 1),
178        _ => format!("{start}-EOF"),
179    }
180}