Skip to main content

steer_tui/tui/widgets/formatters/
dispatch_agent.rs

1use super::{
2    ToolFormatter,
3    helpers::{separator_line, tool_error_user_message, truncate_lines},
4};
5use crate::tui::theme::Theme;
6use ratatui::{
7    style::Style,
8    text::{Line, Span},
9};
10use serde_json::Value;
11use steer_grpc::client_api::{ToolResult, default_agent_spec_id};
12use steer_tools::tools::dispatch_agent::{
13    DispatchAgentParams, DispatchAgentTarget, WorkspaceTarget,
14};
15
16pub struct DispatchAgentFormatter;
17
18impl ToolFormatter for DispatchAgentFormatter {
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::<DispatchAgentParams>(params.clone()) else {
29            return vec![Line::from(Span::styled(
30                "Invalid agent params",
31                theme.error_text(),
32            ))];
33        };
34
35        let preview = if params.prompt.len() > 60 {
36            format!("{}...", &params.prompt[..57])
37        } else {
38            params.prompt.clone()
39        };
40
41        let (agent_id, workspace_summary, params_session_id) = match &params.target {
42            DispatchAgentTarget::New { workspace, agent } => {
43                let agent_id = agent
44                    .as_deref()
45                    .filter(|value| !value.trim().is_empty())
46                    .map_or_else(|| default_agent_spec_id().to_string(), str::to_string);
47                let workspace_summary = match workspace {
48                    WorkspaceTarget::Current => "current".to_string(),
49                    WorkspaceTarget::New { name } => format!("{name} (new)"),
50                };
51                (agent_id, workspace_summary, None)
52            }
53            DispatchAgentTarget::Resume { session_id } => (
54                "resume".to_string(),
55                "session".to_string(),
56                Some(session_id.clone()),
57            ),
58        };
59
60        let session_label = match result {
61            Some(ToolResult::Agent(agent_result)) => agent_result.session_id.clone(),
62            _ => None,
63        }
64        .or(params_session_id);
65
66        let info = match result {
67            Some(ToolResult::Agent(agent_result)) => {
68                let line_count = agent_result.content.lines().count();
69                format!("{line_count} lines")
70            }
71            Some(ToolResult::Error(_)) => "failed".to_string(),
72            Some(_) => "unexpected result type".to_string(),
73            None => "running...".to_string(),
74        };
75
76        lines.push(Line::from(vec![
77            Span::styled(format!("agent={agent_id} "), theme.subtle_text()),
78            Span::styled(
79                format!("workspace={workspace_summary} "),
80                theme.subtle_text(),
81            ),
82            session_label.as_ref().map_or_else(
83                || Span::raw(""),
84                |id| Span::styled(format!("session={id} "), theme.subtle_text()),
85            ),
86            Span::styled(format!("task='{preview}' "), Style::default()),
87            Span::styled(format!("({info})"), theme.subtle_text()),
88        ]));
89
90        lines
91    }
92
93    fn detailed(
94        &self,
95        params: &Value,
96        result: &Option<ToolResult>,
97        wrap_width: usize,
98        theme: &Theme,
99    ) -> Vec<Line<'static>> {
100        let mut lines = Vec::new();
101
102        let Ok(params) = serde_json::from_value::<DispatchAgentParams>(params.clone()) else {
103            return vec![Line::from(Span::styled(
104                "Invalid agent params",
105                theme.error_text(),
106            ))];
107        };
108
109        let (agent_id, workspace_label, params_session_id) = match &params.target {
110            DispatchAgentTarget::New { workspace, agent } => {
111                let agent_id = agent
112                    .as_deref()
113                    .filter(|value| !value.trim().is_empty())
114                    .map_or_else(|| default_agent_spec_id().to_string(), str::to_string);
115                let workspace_label = match workspace {
116                    WorkspaceTarget::Current => "current".to_string(),
117                    WorkspaceTarget::New { name } => format!("{name} (new)"),
118                };
119                (agent_id, workspace_label, None)
120            }
121            DispatchAgentTarget::Resume { session_id } => (
122                "resume".to_string(),
123                "session".to_string(),
124                Some(session_id.clone()),
125            ),
126        };
127
128        lines.push(Line::from(vec![
129            Span::styled("Agent: ", theme.subtle_text()),
130            Span::styled(agent_id, Style::default()),
131        ]));
132        lines.push(Line::from(vec![
133            Span::styled("Workspace: ", theme.subtle_text()),
134            Span::styled(workspace_label, Style::default()),
135        ]));
136        if let Some(session_id) = params_session_id.as_ref() {
137            lines.push(Line::from(vec![
138                Span::styled("Session: ", theme.subtle_text()),
139                Span::styled(session_id.clone(), Style::default()),
140            ]));
141        }
142
143        lines.push(Line::from(Span::styled("Instructions:", theme.text())));
144        for line in params.prompt.lines() {
145            for wrapped_line in textwrap::wrap(line, wrap_width) {
146                lines.push(Line::from(Span::styled(
147                    wrapped_line.to_string(),
148                    Style::default(),
149                )));
150            }
151        }
152
153        // Show output if we have results
154        if let Some(result) = result {
155            match result {
156                ToolResult::Agent(agent_result) => {
157                    if let Some(session_id) = agent_result.session_id.as_ref() {
158                        lines.push(separator_line(wrap_width, theme.dim_text()));
159                        lines.push(Line::from(Span::styled("Session:", theme.subtle_text())));
160                        lines.push(Line::from(Span::styled(
161                            format!("  {session_id}"),
162                            Style::default(),
163                        )));
164                    }
165
166                    if let Some(workspace) = agent_result.workspace.as_ref() {
167                        lines.push(separator_line(wrap_width, theme.dim_text()));
168                        lines.push(Line::from(Span::styled(
169                            "Workspace Revision:",
170                            theme.subtle_text(),
171                        )));
172                        if let Some(workspace_id) = workspace.workspace_id.as_ref() {
173                            lines.push(Line::from(Span::styled(
174                                format!("  id: {workspace_id}"),
175                                Style::default(),
176                            )));
177                        }
178                        if let Some(revision) = workspace.revision.as_ref() {
179                            let kind = revision.vcs_kind.as_str();
180                            let mut summary_line =
181                                format!("  {}: revision {}", kind, revision.revision_id);
182                            if let Some(change_id) = revision.change_id.as_ref() {
183                                summary_line.push_str(&format!("  change {change_id}"));
184                            }
185                            lines.push(Line::from(Span::styled(summary_line, Style::default())));
186                            if !revision.summary.trim().is_empty() {
187                                lines.push(Line::from(Span::styled(
188                                    format!("  summary: {}", revision.summary),
189                                    Style::default(),
190                                )));
191                            }
192                        }
193                    }
194
195                    if !agent_result.content.trim().is_empty() {
196                        lines.push(separator_line(wrap_width, theme.dim_text()));
197
198                        const MAX_OUTPUT_LINES: usize = 30;
199                        let (output_lines, truncated) =
200                            truncate_lines(&agent_result.content, MAX_OUTPUT_LINES);
201
202                        for line in output_lines {
203                            for wrapped in textwrap::wrap(line, wrap_width) {
204                                lines.push(Line::from(Span::raw(wrapped.to_string())));
205                            }
206                        }
207
208                        if truncated {
209                            lines.push(Line::from(Span::styled(
210                                format!(
211                                    "... ({} more lines)",
212                                    agent_result.content.lines().count() - MAX_OUTPUT_LINES
213                                ),
214                                theme.subtle_text(),
215                            )));
216                        }
217                    }
218                }
219                ToolResult::Error(error) => {
220                    lines.push(separator_line(wrap_width, theme.dim_text()));
221                    lines.push(Line::from(Span::styled(
222                        tool_error_user_message(error).into_owned(),
223                        theme.error_text(),
224                    )));
225                }
226                _ => {
227                    lines.push(Line::from(Span::styled(
228                        "Unexpected result type",
229                        theme.error_text(),
230                    )));
231                }
232            }
233        }
234
235        lines
236    }
237
238    fn approval(&self, params: &Value, wrap_width: usize, theme: &Theme) -> Vec<Line<'static>> {
239        let mut lines = Vec::new();
240
241        let Ok(params) = serde_json::from_value::<DispatchAgentParams>(params.clone()) else {
242            return vec![Line::from(Span::styled(
243                "Invalid agent params",
244                theme.error_text(),
245            ))];
246        };
247
248        let (agent_id, workspace_label, params_session_id) = match &params.target {
249            DispatchAgentTarget::New { workspace, agent } => {
250                let agent_id = agent
251                    .as_deref()
252                    .filter(|value| !value.trim().is_empty())
253                    .map_or_else(|| default_agent_spec_id().to_string(), str::to_string);
254                let workspace_label = match workspace {
255                    WorkspaceTarget::Current => "current".to_string(),
256                    WorkspaceTarget::New { name } => format!("{name} (new)"),
257                };
258                (agent_id, workspace_label, None)
259            }
260            DispatchAgentTarget::Resume { session_id } => (
261                "resume".to_string(),
262                "session".to_string(),
263                Some(session_id.clone()),
264            ),
265        };
266
267        lines.push(Line::from(vec![
268            Span::styled("Agent: ", theme.subtle_text()),
269            Span::styled(agent_id, Style::default()),
270        ]));
271        lines.push(Line::from(vec![
272            Span::styled("Workspace: ", theme.subtle_text()),
273            Span::styled(workspace_label, Style::default()),
274        ]));
275        if let Some(session_id) = params_session_id.as_ref() {
276            lines.push(Line::from(vec![
277                Span::styled("Session: ", theme.subtle_text()),
278                Span::styled(session_id.clone(), Style::default()),
279            ]));
280        }
281
282        lines.push(Line::from(Span::styled(
283            "Instructions:",
284            theme.subtle_text(),
285        )));
286        for line in params.prompt.lines() {
287            for wrapped_line in textwrap::wrap(line, wrap_width) {
288                lines.push(Line::from(Span::styled(
289                    wrapped_line.to_string(),
290                    Style::default(),
291                )));
292            }
293        }
294
295        lines
296    }
297}