Skip to main content

codetether_agent/session/helper/
edit.rs

1use 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
150/// Build a confirmation-apply request from a pending edit/multiedit tool call.
151/// Returns `Some((confirm_tool_name, confirm_input))` if the tool call can be
152/// auto-confirmed, or `None` if it is not a confirmable edit tool.
153pub 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}