vtcode_core/tools/registry/
labels.rs1use std::borrow::Cow;
2
3use serde_json::Value;
4
5use crate::config::constants::tools as tool_names;
6use crate::tools::mcp::legacy_mcp_tool_name;
7use crate::tools::tool_intent;
8
9pub fn tool_action_label(tool_name: &str, args: &Value) -> Cow<'static, str> {
10 if tool_intent::is_command_run_tool_call(tool_name, args) {
11 return Cow::Borrowed("Run command");
12 }
13
14 let actual_tool_name = normalize_tool_name(tool_name);
15
16 match actual_tool_name {
17 name if name == tool_names::EXEC_COMMAND => Cow::Borrowed("Run command"),
18 name if name == tool_names::WRITE_STDIN => Cow::Borrowed(write_stdin_action_label(args)),
19 name if name == tool_names::RUN_PTY_CMD => Cow::Borrowed("Run command"),
20 name if name == tool_names::EXECUTE_CODE => Cow::Borrowed("Run code"),
21 name if name == tool_names::GET_ERRORS => Cow::Borrowed("List errors"),
22 name if name == tool_names::MCP => Cow::Borrowed("MCP discovery"),
23 name if name == tool_names::MCP_CONNECT_SERVER => Cow::Borrowed("Connect MCP server"),
24 name if name == tool_names::MCP_DISCONNECT_SERVER => Cow::Borrowed("Disconnect MCP server"),
25 name if name == tool_names::LIST_SKILLS => Cow::Borrowed("List skills"),
26 name if name == tool_names::LOAD_SKILL => Cow::Borrowed("Load skill"),
27 name if name == tool_names::LOAD_SKILL_RESOURCE => Cow::Borrowed("Load skill resource"),
28 name if name == tool_names::READ_FILE => Cow::Borrowed("Read file"),
29 name if name == tool_names::WRITE_FILE => Cow::Borrowed("Write file"),
30 name if name == tool_names::EDIT_FILE => Cow::Borrowed("Edit file"),
31 name if name == tool_names::CREATE_FILE => Cow::Borrowed("Create file"),
32 name if name == tool_names::DELETE_FILE => Cow::Borrowed("Delete file"),
33 name if name == tool_names::APPLY_PATCH => Cow::Borrowed("Apply patch"),
34 name if name == tool_names::SEARCH_REPLACE => Cow::Borrowed("Search/replace"),
35 name if name == tool_names::CREATE_PTY_SESSION => Cow::Borrowed("Create command session"),
36 name if name == tool_names::READ_PTY_SESSION => Cow::Borrowed("Read command session"),
37 name if name == tool_names::LIST_PTY_SESSIONS => Cow::Borrowed("List command sessions"),
38 name if name == tool_names::SEND_PTY_INPUT => Cow::Borrowed("Send command input"),
39 name if name == tool_names::CLOSE_PTY_SESSION => Cow::Borrowed("Close command session"),
40 name if name == tool_names::RESIZE_PTY_SESSION => Cow::Borrowed("Resize command session"),
41 name if name == tool_names::UNIFIED_EXEC => Cow::Borrowed(unified_exec_action_label(args)),
42 name if name == tool_names::CODE_SEARCH => Cow::Borrowed("Search code"),
43 name if name == tool_names::UNIFIED_FILE => match tool_intent::file_operation_action(args).unwrap_or("read") {
44 "read" => Cow::Borrowed("Read file"),
45 "write" => Cow::Borrowed("Write file"),
46 "edit" => Cow::Borrowed("Edit file"),
47 "patch" | tool_names::APPLY_PATCH => Cow::Borrowed("Apply patch"),
48 "delete" => Cow::Borrowed("Delete file"),
49 "move" => Cow::Borrowed("Move file"),
50 "copy" => Cow::Borrowed("Copy file"),
51 _ => Cow::Borrowed("File operation"),
52 },
53 "fetch" | tool_names::WEB_FETCH | tool_names::FETCH_URL | tool_names::DEFUDDLE_FETCH => Cow::Borrowed("Fetch"),
54 _ => Cow::Owned(humanize_tool_name(actual_tool_name)),
55 }
56}
57
58pub fn unified_exec_action_label(args: &Value) -> &'static str {
59 let action = tool_intent::command_session_action(args).unwrap_or("run");
60 if action.eq_ignore_ascii_case("run") {
61 "Run command"
62 } else if action.eq_ignore_ascii_case("write") {
63 "Send command input"
64 } else if action.eq_ignore_ascii_case("poll") {
65 "Read command session"
66 } else if action.eq_ignore_ascii_case("wait") {
67 "Wait for command session"
68 } else if action.eq_ignore_ascii_case("continue") {
69 "Continue command session"
70 } else if action.eq_ignore_ascii_case("inspect") {
71 "Inspect command output"
72 } else if action.eq_ignore_ascii_case("list") {
73 "List command sessions"
74 } else if action.eq_ignore_ascii_case("close") {
75 "Close command session"
76 } else if action.eq_ignore_ascii_case("code") {
77 "Run code"
78 } else {
79 "Exec action"
80 }
81}
82
83pub fn write_stdin_action_label(args: &Value) -> &'static str {
84 if let Some(action) = args.get("action").and_then(Value::as_str) {
85 if action.eq_ignore_ascii_case("wait") {
86 return "Wait for command session";
87 }
88 if action.eq_ignore_ascii_case("poll") {
89 return "Read command session";
90 }
91 if action.eq_ignore_ascii_case("inspect") {
92 return "Inspect command output";
93 }
94 if action.eq_ignore_ascii_case("close") {
95 return "Close command session";
96 }
97 if action.eq_ignore_ascii_case("terminate") {
98 return "Terminate command session";
99 }
100 if action.eq_ignore_ascii_case("write") {
101 return if has_session_input(args) {
104 "Send command input"
105 } else {
106 "Read command session"
107 };
108 }
109 }
110 if has_session_input(args) {
111 "Send command input"
112 } else {
113 "Read command session"
116 }
117}
118
119fn has_session_input(args: &Value) -> bool {
120 ["chars", "input", "text"]
121 .iter()
122 .any(|key| args.get(key).and_then(Value::as_str).is_some_and(|s| !s.is_empty()))
123}
124
125fn normalize_tool_name(tool_name: &str) -> &str {
126 if let Some(stripped) = legacy_mcp_tool_name(tool_name) {
127 return stripped;
128 }
129 if tool_name.starts_with("mcp__") {
130 return tool_name.split("__").last().unwrap_or(tool_name);
131 }
132 if tool_name.starts_with("mcp::") {
133 return tool_name.split("::").last().unwrap_or(tool_name);
134 }
135 tool_name
136}
137
138fn humanize_tool_name(name: &str) -> String {
139 let replaced = name.replace('_', " ");
140 if replaced.is_empty() {
141 return replaced;
142 }
143 let mut chars = replaced.chars();
144 let Some(first) = chars.next() else {
145 return String::new();
146 };
147 let mut result = first.to_uppercase().collect::<String>();
148 result.push_str(&chars.collect::<String>());
149 result
150}
151
152#[cfg(test)]
153mod tests {
154 use super::tool_action_label;
155 use crate::config::constants::tools;
156 use serde_json::json;
157
158 #[test]
159 fn code_search_uses_stable_label() {
160 assert_eq!(tool_action_label(tools::CODE_SEARCH, &json!({"query": "Widget"})), "Search code");
161 }
162
163 #[test]
164 fn write_stdin_distinguishes_wait_from_send() {
165 assert_eq!(
166 tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-wait-1", "action": "wait"})),
167 "Wait for command session"
168 );
169 assert_eq!(
170 tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-send-2", "chars": "y\n"})),
171 "Send command input"
172 );
173 assert_eq!(tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-poll-3"})), "Read command session");
174 }
175
176 #[test]
177 fn unified_exec_wait_is_case_insensitive() {
178 for action in ["wait", "Wait", "WAIT", "WaIt"] {
179 assert_eq!(
180 tool_action_label(tools::UNIFIED_EXEC, &json!({"session_id": "run-abc", "action": action})),
181 "Wait for command session",
182 "action {action} must map case-insensitively"
183 );
184 }
185 }
186
187 #[test]
188 fn write_stdin_explicit_action_wins_over_chars() {
189 let chars = "y\n";
190 assert_eq!(
191 tool_action_label(
192 tools::WRITE_STDIN,
193 &json!({"session_id": "run-explicit-wait", "action": "wait", "chars": chars})
194 ),
195 "Wait for command session"
196 );
197 assert_eq!(
198 tool_action_label(
199 tools::WRITE_STDIN,
200 &json!({"session_id": "run-explicit-write", "action": "write", "chars": chars})
201 ),
202 "Send command input"
203 );
204 }
205
206 #[test]
207 fn write_stdin_empty_vs_whitespace_chars_boundary() {
208 assert_eq!(
209 tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-empty", "chars": ""})),
210 "Read command session"
211 );
212 assert_eq!(
213 tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-space", "chars": " "})),
214 "Send command input",
215 "whitespace-only chars are still stdin bytes"
216 );
217 }
218
219 #[test]
220 fn write_stdin_explicit_write_without_input_reads_as_poll() {
221 assert_eq!(
224 tool_action_label(
225 tools::WRITE_STDIN,
226 &json!({"session_id": "run-write-empty", "action": "write", "chars": ""})
227 ),
228 "Read command session"
229 );
230 assert_eq!(
231 tool_action_label(tools::WRITE_STDIN, &json!({"session_id": "run-write-none", "action": "write"})),
232 "Read command session"
233 );
234 }
235}