Skip to content

Commit 084c0a7

Browse files
arafatkatzeElephant LumpsCline Evaluationpashpashpash
authored
Apply new edit tool to diff (RooCodeInc#3944)
* add ls file tool description, parsing, and return formatting * add json tool definition and remove extra '.' * changeset * use separate function for new format * using json * add grep tool new format * add editTool definition * Map MultiEdit tool to StreamingJsonReplacer with logs * Adding support for non streamed json * Adding logging * moving multiedit tool into tool defs --------- Co-authored-by: Elephant Lumps <[email protected]> Co-authored-by: Cline Evaluation <[email protected]> Co-authored-by: pashpashpash <[email protected]>
1 parent 092bd17 commit 084c0a7

File tree

7 files changed

+277
-119
lines changed

7 files changed

+277
-119
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add grep tool with new parsing format

.changeset/soft-ravens-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Add edit tool definition
Lines changed: 152 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { JSONParser } from "@streamparser/json"
2+
import * as fs from "fs"
3+
import * as path from "path"
4+
import * as os from "os"
25

36
// Fallback type definition based on the error message: "Property 'value' is optional in type 'ParsedElementInfo'"
47
type ParsedElementInfo = {
@@ -9,8 +12,8 @@ type ParsedElementInfo = {
912
}
1013

1114
export interface ReplacementItem {
12-
old_str: string
13-
new_str: string
15+
old_string: string
16+
new_string: string
1417
}
1518

1619
export interface ChangeLocation {
@@ -27,109 +30,243 @@ export class StreamingJsonReplacer {
2730
private onErrorCallback: (error: Error) => void
2831
private itemsProcessed: number = 0
2932
private successfullyParsedItems: ReplacementItem[] = []
33+
private logFilePath: string
3034

3135
constructor(
3236
initialContent: string,
3337
onContentUpdatedCallback: (newContent: string, isFinalItem: boolean, changeLocation?: ChangeLocation) => void,
3438
onErrorCallback: (error: Error) => void,
3539
) {
40+
// Initialize log file path
41+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
42+
this.logFilePath = path.join(os.homedir(), "Documents", `streaming-json-replacer-debug-${timestamp}.log`)
43+
44+
// Initialize log file
45+
this.log("StreamingJsonReplacer Debug Log Started", "INFO")
46+
this.log("Timestamp: " + new Date().toISOString(), "INFO")
47+
this.log("Constructor called with initial content length: " + initialContent.length, "INFO")
48+
this.log("Initial content preview: " + initialContent.substring(0, 200) + "...", "INFO")
49+
3650
this.currentFileContent = initialContent
3751
this.onContentUpdated = onContentUpdatedCallback
3852
this.onErrorCallback = onErrorCallback
3953

40-
this.parser = new JSONParser({ paths: ["$.replacements.*"] })
54+
this.log("Initializing JSONParser with paths: ['$.*']", "INFO")
55+
this.parser = new JSONParser({ paths: ["$.*"] })
4156

4257
this.parser.onValue = (parsedElementInfo: ParsedElementInfo) => {
58+
this.log("onValue callback triggered")
59+
this.log("parsedElementInfo: " + JSON.stringify(parsedElementInfo, null, 2))
60+
4361
const { value } = parsedElementInfo // Destructure to get value, which might be undefined
62+
this.log("Extracted value: " + JSON.stringify(value))
63+
this.log("Value type: " + typeof value)
64+
4465
// This callback is triggered for each item matched by '$.replacements.*'
45-
if (value && typeof value === "object" && "old_str" in value && "new_str" in value) {
66+
if (value && typeof value === "object" && "old_string" in value && "new_string" in value) {
67+
this.log("Found valid replacement item structure")
4668
const item = value as ReplacementItem // Value here is confirmed to be an object
69+
this.log("Replacement item: " + JSON.stringify(item, null, 2))
70+
71+
if (typeof item.old_string === "string" && typeof item.new_string === "string") {
72+
this.log("Item has valid string types for old_string and new_string")
73+
this.log("old_string length: " + item.old_string.length)
74+
this.log("new_string length: " + item.new_string.length)
75+
this.log(
76+
"old_string preview: " +
77+
(item.old_string.substring(0, 100) + (item.old_string.length > 100 ? "..." : "")),
78+
)
79+
this.log(
80+
"new_string preview: " +
81+
(item.new_string.substring(0, 100) + (item.new_string.length > 100 ? "..." : "")),
82+
)
4783

48-
if (typeof item.old_str === "string" && typeof item.new_str === "string") {
4984
this.successfullyParsedItems.push(item) // Store the structurally valid item
85+
this.log("Added item to successfullyParsedItems. Total count: " + this.successfullyParsedItems.length)
86+
87+
if (this.currentFileContent.includes(item.old_string)) {
88+
this.log("old_string found in current file content - proceeding with replacement")
5089

51-
if (this.currentFileContent.includes(item.old_str)) {
5290
// Calculate the change location before making the replacement
53-
const changeLocation = this.calculateChangeLocation(item.old_str, item.new_str)
91+
const changeLocation = this.calculateChangeLocation(item.old_string, item.new_string)
92+
this.log("Calculated change location: " + JSON.stringify(changeLocation))
93+
94+
const beforeLength = this.currentFileContent.length
95+
this.currentFileContent = this.currentFileContent.replace(item.old_string, item.new_string)
96+
const afterLength = this.currentFileContent.length
97+
this.log("Content length before replacement: " + beforeLength)
98+
this.log("Content length after replacement: " + afterLength)
99+
this.log("Length difference: " + (afterLength - beforeLength))
54100

55-
this.currentFileContent = this.currentFileContent.replace(item.old_str, item.new_str)
56101
this.itemsProcessed++
102+
this.log("Incremented itemsProcessed to: " + this.itemsProcessed)
103+
57104
// Notify that an item has been processed. The `isFinalItem` argument here is tricky
58105
// as we don't know from the parser alone if this is the *absolute* last item
59106
// until the stream ends. The caller (Task.ts) will manage the final update.
60107
// For now, we'll pass `false` and let Task.ts handle the final diff view update.
108+
this.log("Calling onContentUpdated callback")
61109
this.onContentUpdated(this.currentFileContent, false, changeLocation)
110+
this.log("onContentUpdated callback completed")
62111
} else {
63-
const snippet = item.old_str.length > 50 ? item.old_str.substring(0, 47) + "..." : item.old_str
64-
const error = new Error(`Streaming Replacement failed: 'old_str' not found. Snippet: "${snippet}"`)
112+
this.log("old_string NOT found in current file content - generating error", "ERROR")
113+
this.log("Current file content length: " + this.currentFileContent.length)
114+
this.log("Current file content preview: " + this.currentFileContent.substring(0, 200) + "...")
115+
116+
const snippet = item.old_string.length > 50 ? item.old_string.substring(0, 47) + "..." : item.old_string
117+
const error = new Error(`Streaming Replacement failed: 'old_string' not found. Snippet: "${snippet}"`)
118+
this.log("Calling onErrorCallback with error: " + error.message, "ERROR")
65119
this.onErrorCallback(error) // Call our own error callback
66120
}
67121
} else {
122+
this.log(
123+
"Invalid string types - old_string type: " +
124+
typeof item.old_string +
125+
", new_string type: " +
126+
typeof item.new_string,
127+
"ERROR",
128+
)
68129
const error = new Error(`Invalid item structure in replacements stream: ${JSON.stringify(item)}`)
130+
this.log("Calling onErrorCallback with error: " + error.message, "ERROR")
69131
this.onErrorCallback(error) // Call our own error callback
70132
}
71133
} else if (value && (Array.isArray(value) || (typeof value === "object" && "replacements" in value))) {
72134
// This might be the 'replacements' array itself or the root object.
73135
// The `paths: ['$.replacements.*']` should mean we only get items.
74136
// If we get here, it's likely the root object if paths wasn't specific enough or if it's an empty replacements array.
75-
console.log("Streaming parser emitted container:", value)
137+
this.log("Streaming parser emitted container: " + JSON.stringify(value))
138+
this.log(
139+
"Container type - isArray: " +
140+
Array.isArray(value) +
141+
", hasReplacements: " +
142+
(typeof value === "object" && "replacements" in value),
143+
)
76144
} else {
77145
// Value is not a ReplacementItem or a known container, could be an issue with the JSON structure or path.
78146
// If `paths` is correct, this path should ideally not be hit often for valid streams.
79-
console.warn("Streaming parser emitted unexpected value:", value)
147+
this.log("Streaming parser emitted unexpected value: " + JSON.stringify(value), "WARN")
148+
this.log("Unexpected value type: " + typeof value, "WARN")
149+
this.log("Has old_string: " + (value && typeof value === "object" && "old_string" in value), "WARN")
150+
this.log("Has new_string: " + (value && typeof value === "object" && "new_string" in value), "WARN")
80151
}
81152
}
82153

83154
this.parser.onError = (err: Error) => {
155+
this.log("Parser onError callback triggered", "ERROR")
156+
this.log("Error details: " + JSON.stringify(err), "ERROR")
157+
this.log("Error message: " + err.message, "ERROR")
158+
this.log("Error stack: " + err.stack, "ERROR")
159+
84160
// Propagate the error to the caller via the callback
161+
this.log("Calling onErrorCallback with parser error", "ERROR")
85162
this.onErrorCallback(err)
86163
// Note: The @streamparser/json library might throw synchronously on write if onError is not set,
87164
// or if it re-throws. We'll ensure Task.ts wraps write/end in try-catch.
88165
}
166+
167+
this.log("Constructor completed - parser setup finished")
168+
169+
// Log to console where the debug file is located
170+
console.log(`[StreamingJsonReplacer] Debug logging to file: ${this.logFilePath}`)
89171
}
90172

91173
public write(jsonChunk: string): void {
92-
// Errors during write will be caught by the parser's onError or thrown.
93-
this.parser.write(jsonChunk)
174+
this.log("write() called")
175+
this.log("JSON chunk length: " + jsonChunk.length)
176+
this.log("JSON chunk preview: " + jsonChunk.substring(0, 200) + (jsonChunk.length > 200 ? "..." : ""))
177+
178+
try {
179+
// Errors during write will be caught by the parser's onError or thrown.
180+
this.log("Calling parser.write()")
181+
this.parser.write(jsonChunk)
182+
this.log("parser.write() completed successfully")
183+
} catch (error) {
184+
this.log("Exception during parser.write(): " + error, "ERROR")
185+
throw error
186+
}
94187
}
95188

96189
public getCurrentContent(): string {
190+
this.log("getCurrentContent() called")
191+
this.log("Current content length: " + this.currentFileContent.length)
97192
return this.currentFileContent
98193
}
99194

100195
public getSuccessfullyParsedItems(): ReplacementItem[] {
196+
this.log("getSuccessfullyParsedItems() called")
197+
this.log("Returning copy of " + this.successfullyParsedItems.length + " items")
101198
return [...this.successfullyParsedItems] // Return a copy
102199
}
103200

104201
private calculateChangeLocation(oldStr: string, newStr: string): ChangeLocation {
202+
this.log("calculateChangeLocation() called")
203+
this.log("oldStr length: " + oldStr.length)
204+
this.log("newStr length: " + newStr.length)
205+
this.log("oldStr preview: " + oldStr.substring(0, 50) + (oldStr.length > 50 ? "..." : ""))
206+
this.log("newStr preview: " + newStr.substring(0, 50) + (newStr.length > 50 ? "..." : ""))
207+
105208
// Find the index where the old string starts
106209
const startIndex = this.currentFileContent.indexOf(oldStr)
210+
this.log("startIndex found: " + startIndex)
211+
107212
if (startIndex === -1) {
213+
this.log("startIndex is -1 - old string not found in content!", "WARN")
214+
this.log("This shouldn't happen since we already checked includes()", "WARN")
108215
// This shouldn't happen since we already checked includes(), but just in case
109216
return { startLine: 0, endLine: 0, startChar: 0, endChar: 0 }
110217
}
111218

112219
// Calculate line numbers by counting newlines before the start index
113220
const contentBeforeStart = this.currentFileContent.substring(0, startIndex)
221+
this.log("contentBeforeStart length: " + contentBeforeStart.length)
222+
114223
const startLine = (contentBeforeStart.match(/\n/g) || []).length
224+
this.log("calculated startLine: " + startLine)
115225

116226
// Calculate the end index after replacement
117227
const endIndex = startIndex + oldStr.length
228+
this.log("calculated endIndex: " + endIndex)
229+
118230
const contentBeforeEnd = this.currentFileContent.substring(0, endIndex)
231+
this.log("contentBeforeEnd length: " + contentBeforeEnd.length)
232+
119233
const endLine = (contentBeforeEnd.match(/\n/g) || []).length
234+
this.log("calculated endLine: " + endLine)
120235

121236
// Calculate character positions within their respective lines
122237
const lastNewlineBeforeStart = contentBeforeStart.lastIndexOf("\n")
238+
this.log("lastNewlineBeforeStart: " + lastNewlineBeforeStart)
239+
123240
const startChar = lastNewlineBeforeStart === -1 ? startIndex : startIndex - lastNewlineBeforeStart - 1
241+
this.log("calculated startChar: " + startChar)
124242

125243
const lastNewlineBeforeEnd = contentBeforeEnd.lastIndexOf("\n")
244+
this.log("lastNewlineBeforeEnd: " + lastNewlineBeforeEnd)
245+
126246
const endChar = lastNewlineBeforeEnd === -1 ? endIndex : endIndex - lastNewlineBeforeEnd - 1
247+
this.log("calculated endChar: " + endChar)
127248

128-
return {
249+
const result = {
129250
startLine,
130251
endLine,
131252
startChar,
132253
endChar,
133254
}
255+
256+
this.log("calculateChangeLocation() returning: " + JSON.stringify(result))
257+
return result
258+
}
259+
260+
private log(message: string, level: "INFO" | "WARN" | "ERROR" = "INFO"): void {
261+
const timestamp = new Date().toISOString()
262+
const logLine = `[${timestamp}] [${level}] ${message}\n`
263+
264+
try {
265+
fs.appendFileSync(this.logFilePath, logLine)
266+
} catch (error) {
267+
// Fallback to console if file logging fails
268+
console.error("Failed to write to log file:", error)
269+
console.log(`[${level}] ${message}`)
270+
}
134271
}
135272
}

src/core/assistant-message/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type AssistantMessageContent = TextContent | ToolUse
22

3-
export { parseAssistantMessageV1, parseAssistantMessageV2 } from "./parse-assistant-message"
3+
export { parseAssistantMessageV1, parseAssistantMessageV2, parseAssistantMessageV3 } from "./parse-assistant-message"
44

55
export interface TextContent {
66
type: "text"

src/core/assistant-message/parse-assistant-message.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,7 @@ export function parseAssistantMessageV3(assistantMessage: string): AssistantMess
539539
if (
540540
inFunctionCalls &&
541541
currentInvokeName === "" &&
542+
!currentToolUse && // Don't create a new tool if we already have one
542543
currentCharIndex >= isInvokeStart.length - 1 &&
543544
assistantMessage.startsWith(isInvokeStart, currentCharIndex - isInvokeStart.length + 1)
544545
) {
@@ -686,6 +687,16 @@ export function parseAssistantMessageV3(assistantMessage: string): AssistantMess
686687
}
687688
}
688689

690+
// If this is a MultiEdit invoke, create a replace_in_file tool
691+
if (currentInvokeName === "MultiEdit") {
692+
currentToolUse = {
693+
type: "tool_use",
694+
name: "replace_in_file",
695+
params: {},
696+
partial: true,
697+
}
698+
}
699+
689700
continue
690701
}
691702
}
@@ -821,6 +832,16 @@ export function parseAssistantMessageV3(assistantMessage: string): AssistantMess
821832
}
822833
}
823834

835+
// Map parameter to tool params for MultiEdit
836+
if (currentToolUse && currentInvokeName === "MultiEdit") {
837+
if (currentParameterName === "file_path") {
838+
currentToolUse.params["path"] = value
839+
} else if (currentParameterName === "edits") {
840+
// Save the value to the diff parameter for replace_in_file
841+
currentToolUse.params["diff"] = value
842+
}
843+
}
844+
824845
currentParameterName = ""
825846
continue
826847
}
@@ -849,13 +870,13 @@ export function parseAssistantMessageV3(assistantMessage: string): AssistantMess
849870
currentInvokeName === "LoadMcpDocumentation" ||
850871
currentInvokeName === "AttemptCompletion" ||
851872
currentInvokeName === "BrowserAction" ||
852-
currentInvokeName === "NewTask")
873+
currentInvokeName === "NewTask" ||
874+
currentInvokeName === "MultiEdit")
853875
) {
854876
currentToolUse.partial = false
855877
contentBlocks.push(currentToolUse)
856878
currentToolUse = undefined
857879
}
858-
859880
currentInvokeName = ""
860881
continue
861882
}
@@ -868,6 +889,12 @@ export function parseAssistantMessageV3(assistantMessage: string): AssistantMess
868889
) {
869890
inFunctionCalls = false
870891
currentTextContentStart = currentCharIndex + 1
892+
// Start a new text content block for any text after function_calls
893+
currentTextContent = {
894+
type: "text",
895+
content: "",
896+
partial: true,
897+
}
871898
continue
872899
}
873900

0 commit comments

Comments
 (0)