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