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}