Skip to main content

dot/tui/
tools.rs

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