Skip to main content

dot/tui/
tools.rs

1#[derive(Debug, Clone, PartialEq)]
2pub enum ToolCategory {
3    FileRead,
4    FileWrite,
5    MultiEdit,
6    Directory,
7    Search,
8    Command,
9    Glob,
10    Grep,
11    WebFetch,
12    Patch,
13    Batch,
14    Snapshot,
15    Question,
16    Mcp { server: String },
17    Skill,
18    Subagent,
19    Unknown,
20}
21
22impl ToolCategory {
23    pub fn from_name(name: &str) -> Self {
24        match name {
25            "read_file" => Self::FileRead,
26            "write_file" => Self::FileWrite,
27            "multiedit" => Self::MultiEdit,
28            "list_directory" => Self::Directory,
29            "search_files" => Self::Search,
30            "run_command" => Self::Command,
31            "glob" => Self::Glob,
32            "grep" => Self::Grep,
33            "webfetch" => Self::WebFetch,
34            "apply_patch" => Self::Patch,
35            "batch" => Self::Batch,
36            "snapshot_list" | "snapshot_restore" => Self::Snapshot,
37            "question" => Self::Question,
38            "skill" => Self::Skill,
39            "subagent" | "subagent_result" => Self::Subagent,
40            other => {
41                if let Some(idx) = other.find('_') {
42                    let prefix = &other[..idx];
43                    if ![
44                        "read", "write", "list", "search", "run", "snapshot", "apply",
45                    ]
46                    .contains(&prefix)
47                    {
48                        return Self::Mcp {
49                            server: prefix.to_string(),
50                        };
51                    }
52                }
53                Self::Unknown
54            }
55        }
56    }
57
58    pub fn icon(&self) -> &'static str {
59        match self {
60            Self::FileRead => "\u{f15c} ",
61            Self::FileWrite => "\u{270e} ",
62            Self::MultiEdit => "\u{270e} ",
63            Self::Directory => "\u{f07b} ",
64            Self::Search => "\u{f002} ",
65            Self::Command => "\u{f120} ",
66            Self::Glob => "\u{f002} ",
67            Self::Grep => "\u{f002} ",
68            Self::WebFetch => "\u{f0ac} ",
69            Self::Patch => "\u{270e} ",
70            Self::Batch => "\u{f0c2} ",
71            Self::Snapshot => "\u{f0c2} ",
72            Self::Question => "\u{f128} ",
73            Self::Mcp { .. } => "\u{f1e6} ",
74            Self::Skill => "\u{f0eb} ",
75            Self::Subagent => "\u{f0c0} ",
76            Self::Unknown => "\u{f013} ",
77        }
78    }
79
80    pub fn label(&self) -> String {
81        match self {
82            Self::FileRead => "read".to_string(),
83            Self::FileWrite => "write".to_string(),
84            Self::MultiEdit => "edit".to_string(),
85            Self::Directory => "list".to_string(),
86            Self::Search => "search".to_string(),
87            Self::Command => "run".to_string(),
88            Self::Glob => "glob".to_string(),
89            Self::Grep => "grep".to_string(),
90            Self::WebFetch => "fetch".to_string(),
91            Self::Patch => "patch".to_string(),
92            Self::Batch => "batch".to_string(),
93            Self::Snapshot => "snapshot".to_string(),
94            Self::Question => "question".to_string(),
95            Self::Mcp { server } => format!("mcp:{}", server),
96            Self::Skill => "skill".to_string(),
97            Self::Subagent => "agent".to_string(),
98            Self::Unknown => "tool".to_string(),
99        }
100    }
101
102    pub fn intent(&self) -> &'static str {
103        match self {
104            Self::FileRead => "reading",
105            Self::FileWrite => "writing",
106            Self::MultiEdit => "editing",
107            Self::Directory => "listing",
108            Self::Search => "searching",
109            Self::Command => "running",
110            Self::Glob => "finding",
111            Self::Grep => "searching",
112            Self::WebFetch => "fetching",
113            Self::Patch => "patching",
114            Self::Batch => "running",
115            Self::Snapshot => "checking",
116            Self::Question => "asking",
117            Self::Mcp { .. } => "calling",
118            Self::Skill => "loading",
119            Self::Subagent => "delegating",
120            Self::Unknown => "running",
121        }
122    }
123}
124
125#[derive(Debug, Clone)]
126pub struct ToolCallDisplay {
127    pub name: String,
128    pub input: String,
129    pub output: Option<String>,
130    pub is_error: bool,
131    pub category: ToolCategory,
132    pub detail: String,
133}
134
135#[derive(Debug, Clone)]
136pub enum StreamSegment {
137    Text(String),
138    ToolCall(ToolCallDisplay),
139}
140
141pub fn extract_tool_detail(name: &str, input: &str) -> String {
142    let parsed: Result<serde_json::Value, _> = serde_json::from_str(input);
143    let val = match parsed {
144        Ok(v) => v,
145        Err(_) => return String::new(),
146    };
147
148    match name {
149        "read_file" => val
150            .get("path")
151            .and_then(|v| v.as_str())
152            .map(shorten_path)
153            .unwrap_or_default(),
154        "write_file" => val
155            .get("path")
156            .and_then(|v| v.as_str())
157            .map(shorten_path)
158            .unwrap_or_default(),
159        "list_directory" => val
160            .get("path")
161            .and_then(|v| v.as_str())
162            .map(shorten_path)
163            .unwrap_or_default(),
164        "search_files" => {
165            let pattern = val.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
166            let path = val.get("path").and_then(|v| v.as_str()).unwrap_or("");
167            if path.is_empty() {
168                format!("\"{}\"", pattern)
169            } else {
170                format!("\"{}\" in {}", pattern, shorten_path(path))
171            }
172        }
173        "run_command" => val
174            .get("command")
175            .and_then(|v| v.as_str())
176            .map(|c| {
177                if c.len() > 60 {
178                    format!("{}...", &c[..57])
179                } else {
180                    c.to_string()
181                }
182            })
183            .unwrap_or_default(),
184        "glob" => val
185            .get("pattern")
186            .and_then(|v| v.as_str())
187            .unwrap_or("")
188            .to_string(),
189        "grep" => {
190            let pattern = val.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
191            let path = val.get("path").and_then(|v| v.as_str()).unwrap_or("");
192            if path.is_empty() {
193                format!("\"{}\"", pattern)
194            } else {
195                format!("\"{}\"; in {}", pattern, shorten_path(path))
196            }
197        }
198        "webfetch" => val
199            .get("url")
200            .and_then(|v| v.as_str())
201            .map(|u| {
202                if u.len() > 60 {
203                    format!("{}...", &u[..57])
204                } else {
205                    u.to_string()
206                }
207            })
208            .unwrap_or_default(),
209        "apply_patch" => {
210            let count = val
211                .get("patches")
212                .and_then(|v| v.as_array())
213                .map(|a| a.len())
214                .unwrap_or(0);
215            format!("{} patches", count)
216        }
217        "multiedit" => {
218            let path = val
219                .get("path")
220                .and_then(|v| v.as_str())
221                .map(shorten_path)
222                .unwrap_or_default();
223            let count = val
224                .get("edits")
225                .and_then(|v| v.as_array())
226                .map(|a| a.len())
227                .unwrap_or(0);
228            format!("{} ({} edits)", path, count)
229        }
230        "batch" => {
231            let count = val
232                .get("invocations")
233                .and_then(|v| v.as_array())
234                .map(|a| a.len())
235                .unwrap_or(0);
236            format!("{} tools", count)
237        }
238        "snapshot_list" => "listing changes".to_string(),
239        "snapshot_restore" => val
240            .get("path")
241            .and_then(|v| v.as_str())
242            .map(shorten_path)
243            .unwrap_or_else(|| "all files".to_string()),
244        "question" => val
245            .get("question")
246            .and_then(|v| v.as_str())
247            .map(|q| {
248                if q.len() > 50 {
249                    format!("{}...", &q[..47])
250                } else {
251                    q.to_string()
252                }
253            })
254            .unwrap_or_default(),
255        "skill" => val
256            .get("name")
257            .and_then(|v| v.as_str())
258            .unwrap_or("")
259            .to_string(),
260        "subagent" => {
261            let desc = val
262                .get("description")
263                .and_then(|v| v.as_str())
264                .map(|d| {
265                    if d.len() > 40 {
266                        format!("{}...", &d[..37])
267                    } else {
268                        d.to_string()
269                    }
270                })
271                .unwrap_or_default();
272            let bg = val
273                .get("background")
274                .and_then(|v| v.as_bool())
275                .unwrap_or(false);
276            if bg { format!("{} (bg)", desc) } else { desc }
277        }
278        "subagent_result" => val
279            .get("id")
280            .and_then(|v| v.as_str())
281            .unwrap_or("")
282            .to_string(),
283        _ => {
284            if let Some(first_str) = val
285                .as_object()
286                .and_then(|o| o.values().find_map(|v| v.as_str().map(|s| s.to_string())))
287            {
288                if first_str.len() > 50 {
289                    format!("{}...", &first_str[..47])
290                } else {
291                    first_str
292                }
293            } else {
294                String::new()
295            }
296        }
297    }
298}
299
300fn shorten_path(path: &str) -> String {
301    if let Ok(home) = std::env::var("HOME")
302        && let Some(rest) = path.strip_prefix(&home)
303    {
304        return format!("~{}", rest);
305    }
306    if let Ok(cwd) = std::env::current_dir() {
307        let cwd_str = cwd.to_string_lossy();
308        if let Some(rest) = path.strip_prefix(cwd_str.as_ref()) {
309            let rest = rest.strip_prefix('/').unwrap_or(rest);
310            return if rest.is_empty() {
311                ".".to_string()
312            } else {
313                format!("./{}", rest)
314            };
315        }
316    }
317    path.to_string()
318}