minion-code 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/cli_entrypoint.py +60 -0
- examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
- examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
- examples/components/messages_component.py +199 -0
- examples/file_freshness_example.py +22 -22
- examples/file_watching_example.py +32 -26
- examples/interruptible_tui.py +921 -3
- examples/repl_tui.py +129 -0
- examples/skills/example_usage.py +57 -0
- examples/start.py +173 -0
- minion_code/__init__.py +1 -1
- minion_code/acp_server/__init__.py +34 -0
- minion_code/acp_server/agent.py +539 -0
- minion_code/acp_server/hooks.py +354 -0
- minion_code/acp_server/main.py +194 -0
- minion_code/acp_server/permissions.py +142 -0
- minion_code/acp_server/test_client.py +104 -0
- minion_code/adapters/__init__.py +22 -0
- minion_code/adapters/output_adapter.py +207 -0
- minion_code/adapters/rich_adapter.py +169 -0
- minion_code/adapters/textual_adapter.py +254 -0
- minion_code/agents/__init__.py +2 -2
- minion_code/agents/code_agent.py +517 -104
- minion_code/agents/hooks.py +378 -0
- minion_code/cli.py +538 -429
- minion_code/cli_simple.py +665 -0
- minion_code/commands/__init__.py +136 -29
- minion_code/commands/clear_command.py +19 -46
- minion_code/commands/help_command.py +33 -49
- minion_code/commands/history_command.py +37 -55
- minion_code/commands/model_command.py +194 -0
- minion_code/commands/quit_command.py +9 -12
- minion_code/commands/resume_command.py +181 -0
- minion_code/commands/skill_command.py +89 -0
- minion_code/commands/status_command.py +48 -73
- minion_code/commands/tools_command.py +54 -52
- minion_code/commands/version_command.py +34 -69
- minion_code/components/ConfirmDialog.py +430 -0
- minion_code/components/Message.py +318 -97
- minion_code/components/MessageResponse.py +30 -29
- minion_code/components/Messages.py +351 -0
- minion_code/components/PromptInput.py +499 -245
- minion_code/components/__init__.py +24 -17
- minion_code/const.py +7 -0
- minion_code/screens/REPL.py +1453 -469
- minion_code/screens/__init__.py +1 -1
- minion_code/services/__init__.py +20 -20
- minion_code/services/event_system.py +19 -14
- minion_code/services/file_freshness_service.py +223 -170
- minion_code/skills/__init__.py +25 -0
- minion_code/skills/skill.py +128 -0
- minion_code/skills/skill_loader.py +198 -0
- minion_code/skills/skill_registry.py +177 -0
- minion_code/subagents/__init__.py +31 -0
- minion_code/subagents/builtin/__init__.py +30 -0
- minion_code/subagents/builtin/claude_code_guide.py +32 -0
- minion_code/subagents/builtin/explore.py +36 -0
- minion_code/subagents/builtin/general_purpose.py +19 -0
- minion_code/subagents/builtin/plan.py +61 -0
- minion_code/subagents/subagent.py +116 -0
- minion_code/subagents/subagent_loader.py +147 -0
- minion_code/subagents/subagent_registry.py +151 -0
- minion_code/tools/__init__.py +8 -2
- minion_code/tools/bash_tool.py +16 -3
- minion_code/tools/file_edit_tool.py +201 -104
- minion_code/tools/file_read_tool.py +183 -26
- minion_code/tools/file_write_tool.py +17 -3
- minion_code/tools/glob_tool.py +23 -2
- minion_code/tools/grep_tool.py +229 -21
- minion_code/tools/ls_tool.py +28 -3
- minion_code/tools/multi_edit_tool.py +89 -84
- minion_code/tools/python_interpreter_tool.py +9 -1
- minion_code/tools/skill_tool.py +210 -0
- minion_code/tools/task_tool.py +287 -0
- minion_code/tools/todo_read_tool.py +28 -24
- minion_code/tools/todo_write_tool.py +82 -65
- minion_code/{types.py → type_defs.py} +15 -2
- minion_code/utils/__init__.py +45 -17
- minion_code/utils/config.py +610 -0
- minion_code/utils/history.py +114 -0
- minion_code/utils/logs.py +53 -0
- minion_code/utils/mcp_loader.py +153 -55
- minion_code/utils/output_truncator.py +233 -0
- minion_code/utils/session_storage.py +369 -0
- minion_code/utils/todo_file_utils.py +26 -22
- minion_code/utils/todo_storage.py +43 -33
- minion_code/web/__init__.py +9 -0
- minion_code/web/adapters/__init__.py +5 -0
- minion_code/web/adapters/web_adapter.py +524 -0
- minion_code/web/api/__init__.py +7 -0
- minion_code/web/api/chat.py +277 -0
- minion_code/web/api/interactions.py +136 -0
- minion_code/web/api/sessions.py +135 -0
- minion_code/web/server.py +149 -0
- minion_code/web/services/__init__.py +5 -0
- minion_code/web/services/session_manager.py +420 -0
- minion_code-0.1.1.dist-info/METADATA +475 -0
- minion_code-0.1.1.dist-info/RECORD +111 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/WHEEL +1 -1
- minion_code-0.1.1.dist-info/entry_points.txt +6 -0
- tests/test_adapter.py +67 -0
- tests/test_adapter_simple.py +79 -0
- tests/test_file_read_tool.py +144 -0
- tests/test_readonly_tools.py +0 -2
- tests/test_skills.py +441 -0
- examples/advance_tui.py +0 -508
- examples/rich_example.py +0 -4
- examples/simple_file_watching.py +0 -57
- examples/simple_tui.py +0 -267
- examples/simple_usage.py +0 -69
- minion_code-0.1.0.dist-info/METADATA +0 -350
- minion_code-0.1.0.dist-info/RECORD +0 -59
- minion_code-0.1.0.dist-info/entry_points.txt +0 -4
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {minion_code-0.1.0.dist-info → minion_code-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,11 @@ import time
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Dict, Any, Optional
|
|
10
10
|
from minion.tools import BaseTool
|
|
11
|
-
from minion_code.services import
|
|
11
|
+
from minion_code.services import (
|
|
12
|
+
record_file_read,
|
|
13
|
+
record_file_edit,
|
|
14
|
+
check_file_freshness,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class FileEditTool(BaseTool):
|
|
@@ -16,27 +20,36 @@ class FileEditTool(BaseTool):
|
|
|
16
20
|
A tool for editing files with string replacement.
|
|
17
21
|
Based on the TypeScript FileEditTool implementation.
|
|
18
22
|
"""
|
|
19
|
-
|
|
23
|
+
|
|
20
24
|
name = "file_edit"
|
|
21
|
-
description = "A tool for editing files by replacing old_string with new_string with freshness tracking"
|
|
25
|
+
description = "A tool for editing files by replacing old_string with new_string with freshness tracking. For large strings (>2000 chars), consider using MultiEditTool or breaking into smaller edits."
|
|
22
26
|
readonly = False
|
|
23
|
-
|
|
27
|
+
|
|
24
28
|
inputs = {
|
|
25
29
|
"file_path": {
|
|
26
30
|
"type": "string",
|
|
27
|
-
"description": "The absolute path to the file to modify"
|
|
31
|
+
"description": "The absolute path to the file to modify",
|
|
28
32
|
},
|
|
29
33
|
"old_string": {
|
|
30
|
-
"type": "string",
|
|
31
|
-
"description": "The text to replace (must be unique within the file)"
|
|
32
|
-
},
|
|
33
|
-
"new_string": {
|
|
34
34
|
"type": "string",
|
|
35
|
-
"description": "The text to replace
|
|
36
|
-
}
|
|
35
|
+
"description": "The text to replace (must be unique within the file)",
|
|
36
|
+
},
|
|
37
|
+
"new_string": {"type": "string", "description": "The text to replace it with"},
|
|
37
38
|
}
|
|
38
39
|
output_type = "string"
|
|
39
|
-
|
|
40
|
+
|
|
41
|
+
def __init__(self, workdir: Optional[str] = None, *args, **kwargs):
|
|
42
|
+
super().__init__(*args, **kwargs)
|
|
43
|
+
self.workdir = Path(workdir) if workdir else None
|
|
44
|
+
|
|
45
|
+
def _resolve_path(self, file_path: str) -> str:
|
|
46
|
+
"""Resolve path using workdir if path is relative."""
|
|
47
|
+
if os.path.isabs(file_path):
|
|
48
|
+
return file_path
|
|
49
|
+
if self.workdir:
|
|
50
|
+
return str(self.workdir / file_path)
|
|
51
|
+
return os.path.abspath(file_path) # Fallback to cwd (backward compatible)
|
|
52
|
+
|
|
40
53
|
def forward(self, file_path: str, old_string: str, new_string: str) -> str:
|
|
41
54
|
"""Execute file edit operation."""
|
|
42
55
|
try:
|
|
@@ -44,195 +57,279 @@ class FileEditTool(BaseTool):
|
|
|
44
57
|
validation_result = self._validate_input(file_path, old_string, new_string)
|
|
45
58
|
if not validation_result["valid"]:
|
|
46
59
|
return f"Error: {validation_result['message']}"
|
|
47
|
-
|
|
60
|
+
|
|
61
|
+
# Check for warnings about large strings
|
|
62
|
+
warning_message = ""
|
|
63
|
+
if "warning" in validation_result:
|
|
64
|
+
warning_message = f"⚠️ Warning: {validation_result['warning']}\n\n"
|
|
65
|
+
|
|
48
66
|
# Apply the edit
|
|
49
67
|
result = self._apply_edit(file_path, old_string, new_string)
|
|
68
|
+
|
|
69
|
+
# Prepend warning if present
|
|
70
|
+
if warning_message:
|
|
71
|
+
result = warning_message + result
|
|
72
|
+
|
|
50
73
|
return result
|
|
51
|
-
|
|
74
|
+
|
|
52
75
|
except Exception as e:
|
|
53
76
|
return f"Error during file edit: {str(e)}"
|
|
54
|
-
|
|
55
|
-
def _validate_input(
|
|
77
|
+
|
|
78
|
+
def _validate_input(
|
|
79
|
+
self, file_path: str, old_string: str, new_string: str
|
|
80
|
+
) -> Dict[str, Any]:
|
|
56
81
|
"""Validate input parameters."""
|
|
57
|
-
|
|
82
|
+
|
|
58
83
|
# Check if old_string and new_string are the same
|
|
59
84
|
if old_string == new_string:
|
|
60
85
|
return {
|
|
61
86
|
"valid": False,
|
|
62
|
-
"message": "No changes to make: old_string and new_string are exactly the same."
|
|
87
|
+
"message": "No changes to make: old_string and new_string are exactly the same.",
|
|
63
88
|
}
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
89
|
+
|
|
90
|
+
# Check for large strings and suggest better alternatives
|
|
91
|
+
large_string_threshold = 2000 # characters
|
|
92
|
+
very_large_threshold = 5000 # characters
|
|
93
|
+
|
|
94
|
+
old_string_size = len(old_string)
|
|
95
|
+
new_string_size = len(new_string)
|
|
96
|
+
max_size = max(old_string_size, new_string_size)
|
|
97
|
+
|
|
98
|
+
if max_size > very_large_threshold:
|
|
99
|
+
suggestions = self._suggest_alternatives_for_large_edit(
|
|
100
|
+
old_string, new_string
|
|
101
|
+
)
|
|
75
102
|
return {
|
|
76
103
|
"valid": False,
|
|
77
|
-
"message": "
|
|
104
|
+
"message": f"String is very large ({max_size} characters). For better performance and reliability, "
|
|
105
|
+
f"large single edits should be avoided as they can be error-prone and difficult to debug.\n\n"
|
|
106
|
+
f"{suggestions}",
|
|
78
107
|
}
|
|
79
|
-
|
|
108
|
+
elif max_size > large_string_threshold:
|
|
109
|
+
# Allow but warn
|
|
110
|
+
lines_count = max(old_string.count("\n"), new_string.count("\n"))
|
|
111
|
+
suggestions = self._suggest_alternatives_for_large_edit(
|
|
112
|
+
old_string, new_string
|
|
113
|
+
)
|
|
114
|
+
warning_msg = (
|
|
115
|
+
f"Large string detected ({max_size} characters, ~{lines_count} lines). "
|
|
116
|
+
)
|
|
117
|
+
if suggestions:
|
|
118
|
+
warning_msg += f"Consider these alternatives:\n{suggestions}"
|
|
119
|
+
else:
|
|
120
|
+
warning_msg += "Consider using MultiEditTool for multiple smaller edits or breaking this into smaller chunks for better reliability and easier debugging."
|
|
121
|
+
|
|
122
|
+
return {"valid": True, "warning": warning_msg}
|
|
123
|
+
|
|
124
|
+
# Resolve path using workdir if relative
|
|
125
|
+
resolved_path = self._resolve_path(file_path)
|
|
126
|
+
|
|
127
|
+
# Handle new file creation
|
|
128
|
+
if not os.path.exists(resolved_path) and old_string == "":
|
|
129
|
+
return {"valid": True}
|
|
130
|
+
|
|
131
|
+
# Check if file exists for existing file edits
|
|
132
|
+
if not os.path.exists(resolved_path):
|
|
133
|
+
return {"valid": False, "message": "File does not exist."}
|
|
134
|
+
|
|
80
135
|
# Check if it's a Jupyter notebook
|
|
81
|
-
if
|
|
136
|
+
if resolved_path.endswith(".ipynb"):
|
|
82
137
|
return {
|
|
83
138
|
"valid": False,
|
|
84
|
-
"message": "File is a Jupyter Notebook. Use NotebookEdit tool instead."
|
|
139
|
+
"message": "File is a Jupyter Notebook. Use NotebookEdit tool instead.",
|
|
85
140
|
}
|
|
86
|
-
|
|
141
|
+
|
|
87
142
|
# Check file freshness (if we have tracking)
|
|
88
143
|
try:
|
|
89
|
-
freshness_result = check_file_freshness(
|
|
144
|
+
freshness_result = check_file_freshness(resolved_path)
|
|
90
145
|
if freshness_result.conflict:
|
|
91
146
|
return {
|
|
92
147
|
"valid": False,
|
|
93
|
-
"message": "File has been modified since last read. Read it again before editing."
|
|
148
|
+
"message": "File has been modified since last read. Read it again before editing.",
|
|
94
149
|
}
|
|
95
150
|
except Exception:
|
|
96
151
|
# If freshness checking fails, continue with basic validation
|
|
97
152
|
pass
|
|
98
|
-
|
|
153
|
+
|
|
99
154
|
# Check if file is binary
|
|
100
|
-
if self._is_binary_file(
|
|
101
|
-
return {
|
|
102
|
-
|
|
103
|
-
"message": "Cannot edit binary files."
|
|
104
|
-
}
|
|
105
|
-
|
|
155
|
+
if self._is_binary_file(resolved_path):
|
|
156
|
+
return {"valid": False, "message": "Cannot edit binary files."}
|
|
157
|
+
|
|
106
158
|
# For existing files, validate old_string exists and is unique
|
|
107
159
|
if old_string != "":
|
|
108
160
|
try:
|
|
109
|
-
with open(
|
|
161
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
110
162
|
content = f.read()
|
|
111
|
-
|
|
163
|
+
|
|
112
164
|
if old_string not in content:
|
|
113
165
|
return {
|
|
114
166
|
"valid": False,
|
|
115
|
-
"message": "String to replace not found in file."
|
|
167
|
+
"message": "String to replace not found in file.",
|
|
116
168
|
}
|
|
117
|
-
|
|
169
|
+
|
|
118
170
|
# Check for multiple matches
|
|
119
171
|
matches = content.count(old_string)
|
|
120
172
|
if matches > 1:
|
|
121
173
|
return {
|
|
122
174
|
"valid": False,
|
|
123
175
|
"message": f"Found {matches} matches of the string to replace. "
|
|
124
|
-
|
|
125
|
-
|
|
176
|
+
"For safety, this tool only supports replacing exactly one occurrence at a time. "
|
|
177
|
+
"Add more lines of context to your edit and try again.",
|
|
126
178
|
}
|
|
127
|
-
|
|
179
|
+
|
|
128
180
|
except UnicodeDecodeError:
|
|
129
181
|
return {
|
|
130
182
|
"valid": False,
|
|
131
|
-
"message": "Cannot read file - appears to be binary or has encoding issues."
|
|
183
|
+
"message": "Cannot read file - appears to be binary or has encoding issues.",
|
|
132
184
|
}
|
|
133
|
-
|
|
185
|
+
|
|
134
186
|
return {"valid": True}
|
|
135
|
-
|
|
187
|
+
|
|
136
188
|
def _apply_edit(self, file_path: str, old_string: str, new_string: str) -> str:
|
|
137
189
|
"""Apply the edit to the file."""
|
|
138
|
-
|
|
139
|
-
# Resolve
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
|
|
191
|
+
# Resolve path using workdir if relative
|
|
192
|
+
resolved_path = self._resolve_path(file_path)
|
|
193
|
+
|
|
143
194
|
# Handle new file creation
|
|
144
195
|
if old_string == "":
|
|
145
196
|
# Create new file
|
|
146
|
-
os.makedirs(os.path.dirname(
|
|
147
|
-
|
|
148
|
-
with open(
|
|
197
|
+
os.makedirs(os.path.dirname(resolved_path), exist_ok=True)
|
|
198
|
+
|
|
199
|
+
with open(resolved_path, "w", encoding="utf-8") as f:
|
|
149
200
|
f.write(new_string)
|
|
150
|
-
|
|
201
|
+
|
|
151
202
|
# Record the file edit
|
|
152
|
-
record_file_edit(
|
|
153
|
-
|
|
154
|
-
return f"Successfully created new file: {
|
|
155
|
-
|
|
203
|
+
record_file_edit(resolved_path, new_string)
|
|
204
|
+
|
|
205
|
+
return f"Successfully created new file: {resolved_path}"
|
|
206
|
+
|
|
156
207
|
# Edit existing file
|
|
157
208
|
try:
|
|
158
209
|
# Read current content
|
|
159
|
-
with open(
|
|
210
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
160
211
|
original_content = f.read()
|
|
161
|
-
|
|
212
|
+
|
|
162
213
|
# Apply replacement
|
|
163
214
|
if new_string == "":
|
|
164
215
|
# Handle deletion - check if we need to remove trailing newline
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
216
|
+
if (
|
|
217
|
+
not old_string.endswith("\n")
|
|
218
|
+
and original_content.find(old_string + "\n") != -1
|
|
219
|
+
):
|
|
220
|
+
updated_content = original_content.replace(
|
|
221
|
+
old_string + "\n", new_string
|
|
222
|
+
)
|
|
168
223
|
else:
|
|
169
224
|
updated_content = original_content.replace(old_string, new_string)
|
|
170
225
|
else:
|
|
171
226
|
updated_content = original_content.replace(old_string, new_string)
|
|
172
|
-
|
|
227
|
+
|
|
173
228
|
# Verify the replacement worked
|
|
174
229
|
if updated_content == original_content:
|
|
175
230
|
return "Error: Original and edited file match exactly. Failed to apply edit."
|
|
176
|
-
|
|
231
|
+
|
|
177
232
|
# Write updated content
|
|
178
|
-
with open(
|
|
233
|
+
with open(resolved_path, "w", encoding="utf-8") as f:
|
|
179
234
|
f.write(updated_content)
|
|
180
|
-
|
|
235
|
+
|
|
181
236
|
# Record the file edit
|
|
182
|
-
record_file_edit(
|
|
183
|
-
|
|
237
|
+
record_file_edit(resolved_path, updated_content)
|
|
238
|
+
|
|
184
239
|
# Generate result message with snippet
|
|
185
240
|
snippet_info = self._get_snippet(original_content, old_string, new_string)
|
|
186
|
-
|
|
187
|
-
result = f"The file {
|
|
188
|
-
result += self._add_line_numbers(
|
|
189
|
-
|
|
241
|
+
|
|
242
|
+
result = f"The file {resolved_path} has been updated. Here's the result of the edit:\n"
|
|
243
|
+
result += self._add_line_numbers(
|
|
244
|
+
snippet_info["snippet"], snippet_info["start_line"]
|
|
245
|
+
)
|
|
246
|
+
|
|
190
247
|
return result
|
|
191
|
-
|
|
248
|
+
|
|
192
249
|
except Exception as e:
|
|
193
250
|
return f"Error applying edit: {str(e)}"
|
|
194
|
-
|
|
251
|
+
|
|
195
252
|
def _is_binary_file(self, file_path: str) -> bool:
|
|
196
253
|
"""Check if file is binary."""
|
|
197
254
|
try:
|
|
198
|
-
with open(file_path,
|
|
255
|
+
with open(file_path, "rb") as f:
|
|
199
256
|
chunk = f.read(1024)
|
|
200
|
-
return b
|
|
257
|
+
return b"\0" in chunk
|
|
201
258
|
except Exception:
|
|
202
259
|
return False
|
|
203
|
-
|
|
204
|
-
def _get_snippet(
|
|
205
|
-
|
|
260
|
+
|
|
261
|
+
def _get_snippet(
|
|
262
|
+
self,
|
|
263
|
+
original_content: str,
|
|
264
|
+
old_string: str,
|
|
265
|
+
new_string: str,
|
|
266
|
+
context_lines: int = 4,
|
|
267
|
+
) -> Dict[str, Any]:
|
|
206
268
|
"""Get a snippet of the file showing the change with context."""
|
|
207
|
-
|
|
269
|
+
|
|
208
270
|
# Find the replacement position
|
|
209
271
|
before_replacement = original_content.split(old_string)[0]
|
|
210
|
-
replacement_line = before_replacement.count(
|
|
211
|
-
|
|
272
|
+
replacement_line = before_replacement.count("\n")
|
|
273
|
+
|
|
212
274
|
# Create the new content
|
|
213
275
|
new_content = original_content.replace(old_string, new_string)
|
|
214
|
-
new_lines = new_content.split(
|
|
215
|
-
|
|
276
|
+
new_lines = new_content.split("\n")
|
|
277
|
+
|
|
216
278
|
# Calculate snippet boundaries
|
|
217
279
|
start_line = max(0, replacement_line - context_lines)
|
|
218
|
-
end_line = min(
|
|
219
|
-
|
|
280
|
+
end_line = min(
|
|
281
|
+
len(new_lines),
|
|
282
|
+
replacement_line + context_lines + new_string.count("\n") + 1,
|
|
283
|
+
)
|
|
284
|
+
|
|
220
285
|
# Extract snippet
|
|
221
286
|
snippet_lines = new_lines[start_line:end_line]
|
|
222
|
-
snippet =
|
|
223
|
-
|
|
287
|
+
snippet = "\n".join(snippet_lines)
|
|
288
|
+
|
|
224
289
|
return {
|
|
225
|
-
|
|
226
|
-
|
|
290
|
+
"snippet": snippet,
|
|
291
|
+
"start_line": start_line + 1, # Convert to 1-based line numbers
|
|
227
292
|
}
|
|
228
|
-
|
|
293
|
+
|
|
294
|
+
def _suggest_alternatives_for_large_edit(
|
|
295
|
+
self, old_string: str, new_string: str
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Suggest alternative approaches for large string edits."""
|
|
298
|
+
old_lines = old_string.count("\n") + 1
|
|
299
|
+
new_lines = new_string.count("\n") + 1
|
|
300
|
+
|
|
301
|
+
suggestions = []
|
|
302
|
+
|
|
303
|
+
if old_lines > 20 or new_lines > 20:
|
|
304
|
+
suggestions.append(
|
|
305
|
+
"• Use MultiEditTool to break this into multiple smaller string replacements"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if len(old_string) > 3000 or len(new_string) > 3000:
|
|
309
|
+
suggestions.append(
|
|
310
|
+
"• Consider using FileWriteTool to rewrite the entire file if making extensive changes"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if old_lines > 10:
|
|
314
|
+
suggestions.append(
|
|
315
|
+
"• Break the edit into smaller, more focused string replacements"
|
|
316
|
+
)
|
|
317
|
+
suggestions.append(
|
|
318
|
+
"• Use more specific context to make smaller, safer edits"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if suggestions:
|
|
322
|
+
return "Alternative approaches for large edits:\n" + "\n".join(suggestions)
|
|
323
|
+
|
|
324
|
+
return ""
|
|
325
|
+
|
|
229
326
|
def _add_line_numbers(self, content: str, start_line: int = 1) -> str:
|
|
230
327
|
"""Add line numbers to content."""
|
|
231
|
-
lines = content.split(
|
|
328
|
+
lines = content.split("\n")
|
|
232
329
|
numbered_lines = []
|
|
233
|
-
|
|
330
|
+
|
|
234
331
|
for i, line in enumerate(lines):
|
|
235
332
|
line_num = start_line + i
|
|
236
333
|
numbered_lines.append(f"{line_num:6d} {line}")
|
|
237
|
-
|
|
238
|
-
return
|
|
334
|
+
|
|
335
|
+
return "\n".join(numbered_lines)
|