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