steer_tui/tui/widgets/formatters/
view.rs1use 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(¶ms.file_path)
33 .file_name()
34 .and_then(|s| s.to_str())
35 .unwrap_or(¶ms.file_path);
36
37 let mut spans = vec![Span::styled(file_name.to_string(), Style::default())];
38
39 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 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 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 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 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 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 tool_error_user_message(error).into_owned(),
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}