codetether_agent/session/helper/
edit.rs1use serde_json::Value;
2use std::collections::HashMap;
3
4fn stub_marker_in_text(text: &str) -> Option<&'static str> {
5 let lower = text.to_ascii_lowercase();
6 let markers = [
7 "todo",
8 "fixme",
9 "placeholder implementation",
10 "<placeholder>",
11 "[placeholder]",
12 "{placeholder}",
13 "not implemented",
14 "fallback",
15 "stub",
16 "coming soon",
17 "unimplemented!(",
18 "todo!(",
19 "throw new error(\"not implemented",
20 ];
21 markers.into_iter().find(|m| lower.contains(m))
22}
23
24pub fn detect_stub_in_tool_input(tool_name: &str, tool_input: &Value) -> Option<String> {
25 let check = |label: &str, text: &str| {
26 stub_marker_in_text(text).map(|marker| format!("{label} contains stub marker \"{marker}\""))
27 };
28
29 match tool_name {
30 "write" => tool_input
31 .get("content")
32 .and_then(Value::as_str)
33 .and_then(|text| check("content", text)),
34 "edit" | "confirm_edit" => tool_input
35 .get("new_string")
36 .or_else(|| tool_input.get("newString"))
37 .and_then(Value::as_str)
38 .and_then(|text| check("new_string", text)),
39 "advanced_edit" => tool_input
40 .get("newString")
41 .and_then(Value::as_str)
42 .and_then(|text| check("newString", text)),
43 "multiedit" | "confirm_multiedit" => {
44 let edits = tool_input.get("edits").and_then(Value::as_array)?;
45 for (idx, edit) in edits.iter().enumerate() {
46 if let Some(reason) = edit
47 .get("new_string")
48 .or_else(|| edit.get("newString"))
49 .and_then(Value::as_str)
50 .and_then(|text| check(&format!("edits[{idx}].new_string"), text))
51 {
52 return Some(reason);
53 }
54 }
55 None
56 }
57 "patch" => {
58 let patch = tool_input.get("patch").and_then(Value::as_str)?;
59 let added_lines = patch
60 .lines()
61 .filter(|line| line.starts_with('+') && !line.starts_with("+++"))
62 .map(|line| line.trim_start_matches('+'))
63 .collect::<Vec<_>>()
64 .join("\n");
65 if added_lines.is_empty() {
66 None
67 } else {
68 check("patch additions", &added_lines)
69 }
70 }
71 _ => None,
72 }
73}
74
75pub fn normalize_tool_call_for_execution(tool_name: &str, tool_input: &Value) -> (String, Value) {
76 let mut normalized_name = tool_name.to_string();
77 let mut normalized_input = tool_input.clone();
78
79 if tool_name == "advanced_edit" {
80 normalized_name = "edit".to_string();
81 }
82
83 if matches!(
84 normalized_name.as_str(),
85 "edit" | "confirm_edit" | "advanced_edit"
86 ) {
87 if let Some(obj) = normalized_input.as_object_mut() {
88 if obj.get("path").is_none() {
89 if let Some(v) = obj.get("filePath").cloned() {
90 obj.insert("path".to_string(), v);
91 } else if let Some(v) = obj.get("file").cloned() {
92 obj.insert("path".to_string(), v);
93 }
94 }
95 if obj.get("old_string").is_none()
96 && let Some(v) = obj.get("oldString").cloned()
97 {
98 obj.insert("old_string".to_string(), v);
99 }
100 if obj.get("new_string").is_none()
101 && let Some(v) = obj.get("newString").cloned()
102 {
103 obj.insert("new_string".to_string(), v);
104 }
105 }
106 }
107
108 if matches!(normalized_name.as_str(), "multiedit" | "confirm_multiedit")
109 && let Some(obj) = normalized_input.as_object_mut()
110 {
111 if obj.get("edits").is_none() {
112 if let Some(v) = obj.get("changes").cloned() {
113 obj.insert("edits".to_string(), v);
114 } else if let Some(v) = obj.get("operations").cloned() {
115 obj.insert("edits".to_string(), v);
116 } else if obj.get("file").is_some() || obj.get("path").is_some() {
117 let single = Value::Object(obj.clone());
118 obj.insert("edits".to_string(), Value::Array(vec![single]));
119 }
120 }
121
122 if let Some(edits) = obj.get_mut("edits").and_then(Value::as_array_mut) {
123 for edit in edits {
124 if let Some(edit_obj) = edit.as_object_mut() {
125 if edit_obj.get("file").is_none() {
126 if let Some(v) = edit_obj.get("filePath").cloned() {
127 edit_obj.insert("file".to_string(), v);
128 } else if let Some(v) = edit_obj.get("path").cloned() {
129 edit_obj.insert("file".to_string(), v);
130 }
131 }
132 if edit_obj.get("old_string").is_none()
133 && let Some(v) = edit_obj.get("oldString").cloned()
134 {
135 edit_obj.insert("old_string".to_string(), v);
136 }
137 if edit_obj.get("new_string").is_none()
138 && let Some(v) = edit_obj.get("newString").cloned()
139 {
140 edit_obj.insert("new_string".to_string(), v);
141 }
142 }
143 }
144 }
145 }
146
147 (normalized_name, normalized_input)
148}
149
150pub fn build_pending_confirmation_apply_request(
154 tool_name: &str,
155 tool_input: &Value,
156 _tool_metadata: Option<&HashMap<String, Value>>,
157) -> Option<(String, Value)> {
158 let confirm_tool_name = match tool_name {
159 "edit" | "confirm_edit" | "advanced_edit" => "confirm_edit",
160 "multiedit" | "confirm_multiedit" => "confirm_multiedit",
161 _ => return None,
162 };
163
164 let (_, mut normalized) = normalize_tool_call_for_execution(tool_name, tool_input);
165 if let Some(obj) = normalized.as_object_mut() {
166 obj.insert("confirm".to_string(), Value::Bool(true));
167 }
168
169 Some((confirm_tool_name.to_string(), normalized))
170}