Skip to content

Commit 508e6e5

Browse files
committed
Improve plan mode to support file changes with diffs
1 parent 6831a34 commit 508e6e5

File tree

7 files changed

+71
-27
lines changed

7 files changed

+71
-27
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Improve plan-mode to do file changes with diffs.
6+
57
## 0.24.3
68

79
- Fix initializationOptions config merge.

docs/configuration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ interface Config {
200200
systemPromptTemplateFile?: string;
201201
nativeTools: {
202202
filesystem: {enabled: boolean};
203-
shell: {enabled: boolean;
204-
excludeCommands: string[]};
203+
shell: {enabled: boolean,
204+
excludeCommands: string[]};
205205
};
206206
disabledTools: string[],
207207
toolCall?: {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits).
1+
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received (for example, to make edits). The only allowed tool related to code changes is the `eca_plan_edit_file` since system will take care to present to user the changes, only run if user ask to apply changes.

src/eca/features/chat.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
repo-map* (delay (f.index/repo-map db {:as-string? true}))
116116
instructions (f.prompt/build-instructions refined-contexts rules repo-map* (or behavior (:chat-default-behavior db)) config)
117117
past-messages (get-in db [:chats chat-id :messages] [])
118-
all-tools (f.tools/all-tools @db* config)
118+
all-tools (f.tools/all-tools behavior @db* config)
119119
received-msgs* (atom "")
120120
received-thinking* (atom "")
121121
tool-call-by-id* (atom {:args {}})

src/eca/features/tools.clj

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"This ns centralizes all available tools for LLMs including
33
eca native tools and MCP servers."
44
(:require
5+
[babashka.fs :as fs]
56
[clojure.string :as string]
67
[eca.diff :as diff]
78
[eca.features.tools.filesystem :as f.tools.filesystem]
@@ -10,8 +11,7 @@
1011
[eca.features.tools.util :as tools.util]
1112
[eca.logger :as logger]
1213
[eca.messenger :as messenger]
13-
[eca.shared :refer [assoc-some]]
14-
[babashka.fs :as fs])
14+
[eca.shared :refer [assoc-some]])
1515
(:import
1616
[java.util Map]))
1717

@@ -39,11 +39,16 @@
3939
(defn all-tools
4040
"Returns all available tools, including both native ECA tools
4141
(like filesystem and shell tools) and tools provided by MCP servers."
42-
[db config]
42+
[behavior db config]
4343
(let [disabled-tools (set (get-in config [:disabledTools] []))]
4444
(filterv
4545
(fn [tool]
46-
(not (contains? disabled-tools (:name tool))))
46+
(and (not (contains? disabled-tools (:name tool)))
47+
;; check for enabled-fn if present
48+
((or (:enabled-fn tool) (constantly true))
49+
{:behavior behavior
50+
:db db
51+
:config config})))
4752
(concat
4853
(mapv #(assoc % :origin :native) (native-tools db config))
4954
(mapv #(assoc % :origin :mcp) (f.mcp/all-tools db))))))
@@ -127,17 +132,18 @@
127132
:linesAdded added
128133
:linesRemoved removed
129134
:diff diff})))
130-
"eca_edit_file" (let [path (get arguments "path")
131-
original-content (get arguments "original_content")
132-
new-content (get arguments "new_content")
133-
all? (get arguments "all_occurrences")]
134-
(when-let [{:keys [original-full-content
135-
new-full-content]} (and path (fs/exists? path) original-content new-content
136-
(f.tools.filesystem/file-change-full-content path original-content new-content all?))]
137-
(let [{:keys [added removed diff]} (diff/diff original-full-content new-full-content path)]
138-
{:type :fileChange
139-
:path path
140-
:linesAdded added
141-
:linesRemoved removed
142-
:diff diff})))
135+
("eca_plan_edit_file"
136+
"eca_edit_file") (let [path (get arguments "path")
137+
original-content (get arguments "original_content")
138+
new-content (get arguments "new_content")
139+
all? (get arguments "all_occurrences")]
140+
(when-let [{:keys [original-full-content
141+
new-full-content]} (and path (fs/exists? path) original-content new-content
142+
(f.tools.filesystem/file-change-full-content path original-content new-content all?))]
143+
(let [{:keys [added removed diff]} (diff/diff original-full-content new-full-content path)]
144+
{:type :fileChange
145+
:path path
146+
:linesAdded added
147+
:linesRemoved removed
148+
:diff diff})))
143149
nil))

src/eca/features/tools/filesystem.clj

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,19 +171,32 @@
171171
{:original-full-content original-full-content
172172
:new-full-content new-full-content})))
173173

174-
(defn ^:private edit-file [arguments {:keys [db]}]
174+
(defn ^:private change-file [arguments {:keys [db]} diff?]
175175
(or (tools.util/invalid-arguments arguments (concat (path-validations db)
176176
[["path" fs/readable? "File $path is not readable"]]))
177177
(let [path (get arguments "path")
178178
original-content (get arguments "original_content")
179179
new-content (get arguments "new_content")
180+
new-content (if diff?
181+
(str "<<<<<<< HEAD\n"
182+
original-content
183+
"\n=======\n"
184+
new-content
185+
"\n>>>>>>> eca\n")
186+
new-content)
180187
all? (boolean (get arguments "all_occurrences"))]
181188
(if-let [{:keys [new-full-content]} (file-change-full-content path original-content new-content all?)]
182189
(do
183190
(spit path new-full-content)
184191
(tools.util/single-text-content (format "Successfully replaced content in %s." path)))
185192
(tools.util/single-text-content (format "Original content not found in %s" path) :error)))))
186193

194+
(defn ^:private edit-file [arguments components]
195+
(change-file arguments components false))
196+
197+
(defn ^:private plan-edit-file [arguments components]
198+
(change-file arguments components true))
199+
187200
(defn ^:private move-file [arguments {:keys [db]}]
188201
(let [workspace-dirs (tools.util/workspace-roots-strs db)]
189202
(or (tools.util/invalid-arguments arguments [["source" fs/exists? "$source is not a valid path"]
@@ -246,8 +259,9 @@
246259
{:description (str "Replace a specific string or content block in a file with new content. "
247260
"Finds the exact original content and replaces it with new content. "
248261
"Be extra careful to format the original-content exactly correctly, "
249-
"taking extra care with whitespace and newlines. In addition to replacing strings, "
250-
"this can also be used to prepend, append, or delete contents from a file.")
262+
"taking extra care with whitespace and newlines. "
263+
"Avoid replacing whole functions, methods, or classes, change only the needed code. "
264+
"In addition to replacing strings, this can also be used to prepend, append, or delete contents from a file.")
251265
:parameters {:type "object"
252266
:properties {"path" {:type "string"
253267
:description "The absolute file path to do the replace."}
@@ -259,7 +273,29 @@
259273
:description "Whether to replace all occurences of the file or just the first one (default)"}}
260274
:required ["path" "original_content" "new_content"]}
261275
:handler #'edit-file
276+
:enabled-fn (fn [{:keys [behavior]}] (not= "plan" behavior))
262277
:summary-fn (constantly "Editting file")}
278+
"eca_plan_edit_file"
279+
{:description (str "Plan a file change where user needs to apply or reject the change. "
280+
"Replace a specific string or content block in a file with new content. "
281+
"Finds the exact original content and replaces it with new content. "
282+
"Be extra careful to format the original-content exactly correctly, "
283+
"taking extra care with whitespace and newlines. "
284+
"Avoid replacing whole functions, methods, or classes, change only the needed code. "
285+
"In addition to replacing strings, this can also be used to prepend, append, or delete contents from a file.")
286+
:parameters {:type "object"
287+
:properties {"path" {:type "string"
288+
:description "The absolute file path to do the replace."}
289+
"original_content" {:type "string"
290+
:description "The exact content to find and replace"}
291+
"new_content" {:type "string"
292+
:description "The new content to replace the original content with"}
293+
"all_occurrences" {:type "boolean"
294+
:description "Whether to replace all occurences of the file or just the first one (default)"}}
295+
:required ["path" "original_content" "new_content"]}
296+
:handler #'plan-edit-file
297+
:enabled-fn (fn [{:keys [behavior]}] (= "plan" behavior))
298+
:summary-fn (constantly "Planning edit")}
263299
"eca_move_file"
264300
{:description (str "Move or rename files and directories. Can move files between directories "
265301
"and rename them in a single operation. If the destination exists, the "

src/eca/main.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
(System/exit (or status 1)))
1818

1919
(defn ^:private version []
20-
(->> [(str "eca " (config/eca-version))]
21-
(string/join \newline)))
20+
(str "eca " (config/eca-version)))
2221

23-
(defn ^:private help [options-summary]
22+
(defn ^:private help
23+
[options-summary]
2424
(->> ["ECA - Editor Code Assistant"
2525
""
2626
"Usage: eca <command> [<options>]"

0 commit comments

Comments
 (0)