Skip to content

Commit 3d76825

Browse files
committed
♻️ refactor(Cline): Implement insertContentTool
1 parent e7e5511 commit 3d76825

File tree

2 files changed

+190
-148
lines changed

2 files changed

+190
-148
lines changed

src/core/Cline.ts

Lines changed: 14 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,10 @@ import {
2222
RepoPerWorkspaceCheckpointService,
2323
} from "../services/checkpoints"
2424
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
25-
import {
26-
extractTextFromFile,
27-
addLineNumbers,
28-
stripLineNumbers,
29-
everyLineHasLineNumbers,
30-
} from "../integrations/misc/extract-text"
31-
import { countFileLines } from "../integrations/misc/line-counter"
25+
import { addLineNumbers, stripLineNumbers, everyLineHasLineNumbers } from "../integrations/misc/extract-text"
3226
import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
3327
import { readFileTool } from "./tools/readFileTool"
28+
import { insertContentTool } from "./tools/insertContentTool"
3429
import { ExitCodeDetails } from "../integrations/terminal/TerminalProcess"
3530
import { Terminal } from "../integrations/terminal/Terminal"
3631
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
@@ -1941,149 +1936,20 @@ export class Cline extends EventEmitter<ClineEvents> {
19411936
}
19421937

19431938
case "insert_content": {
1944-
const relPath: string | undefined = block.params.path
1945-
const operations: string | undefined = block.params.operations
1946-
1947-
const sharedMessageProps: ClineSayTool = {
1948-
tool: "appliedDiff",
1949-
path: getReadablePath(this.cwd, removeClosingTag("path", relPath)),
1939+
const didEditFileEmitter = (b: boolean) => {
1940+
this.didEditFile = b
19501941
}
19511942

1952-
try {
1953-
if (block.partial) {
1954-
const partialMessage = JSON.stringify(sharedMessageProps)
1955-
await this.ask("tool", partialMessage, block.partial).catch(() => {})
1956-
break
1957-
}
1958-
1959-
// Validate required parameters
1960-
if (!relPath) {
1961-
this.consecutiveMistakeCount++
1962-
pushToolResult(await this.sayAndCreateMissingParamError("insert_content", "path"))
1963-
break
1964-
}
1965-
1966-
if (!operations) {
1967-
this.consecutiveMistakeCount++
1968-
pushToolResult(await this.sayAndCreateMissingParamError("insert_content", "operations"))
1969-
break
1970-
}
1971-
1972-
const absolutePath = path.resolve(this.cwd, relPath)
1973-
const fileExists = await fileExistsAtPath(absolutePath)
1974-
1975-
if (!fileExists) {
1976-
this.consecutiveMistakeCount++
1977-
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
1978-
await this.say("error", formattedError)
1979-
pushToolResult(formattedError)
1980-
break
1981-
}
1982-
1983-
let parsedOperations: Array<{
1984-
start_line: number
1985-
content: string
1986-
}>
1987-
1988-
try {
1989-
parsedOperations = JSON.parse(operations)
1990-
if (!Array.isArray(parsedOperations)) {
1991-
throw new Error("Operations must be an array")
1992-
}
1993-
} catch (error) {
1994-
this.consecutiveMistakeCount++
1995-
await this.say("error", `Failed to parse operations JSON: ${error.message}`)
1996-
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
1997-
break
1998-
}
1999-
2000-
this.consecutiveMistakeCount = 0
2001-
2002-
// Read the file
2003-
const fileContent = await fs.readFile(absolutePath, "utf8")
2004-
this.diffViewProvider.editType = "modify"
2005-
this.diffViewProvider.originalContent = fileContent
2006-
const lines = fileContent.split("\n")
2007-
2008-
const updatedContent = insertGroups(
2009-
lines,
2010-
parsedOperations.map((elem) => {
2011-
return {
2012-
index: elem.start_line - 1,
2013-
elements: elem.content.split("\n"),
2014-
}
2015-
}),
2016-
).join("\n")
2017-
2018-
// Show changes in diff view
2019-
if (!this.diffViewProvider.isEditing) {
2020-
await this.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
2021-
// First open with original content
2022-
await this.diffViewProvider.open(relPath)
2023-
await this.diffViewProvider.update(fileContent, false)
2024-
this.diffViewProvider.scrollToFirstDiff()
2025-
await delay(200)
2026-
}
2027-
2028-
const diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent)
2029-
2030-
if (!diff) {
2031-
pushToolResult(`No changes needed for '${relPath}'`)
2032-
break
2033-
}
2034-
2035-
await this.diffViewProvider.update(updatedContent, true)
2036-
2037-
const completeMessage = JSON.stringify({
2038-
...sharedMessageProps,
2039-
diff,
2040-
} satisfies ClineSayTool)
2041-
2042-
const didApprove = await this.ask("tool", completeMessage, false).then(
2043-
(response) => response.response === "yesButtonClicked",
2044-
)
2045-
2046-
if (!didApprove) {
2047-
await this.diffViewProvider.revertChanges()
2048-
pushToolResult("Changes were rejected by the user.")
2049-
break
2050-
}
2051-
2052-
const { newProblemsMessage, userEdits, finalContent } =
2053-
await this.diffViewProvider.saveChanges()
2054-
this.didEditFile = true
2055-
2056-
if (!userEdits) {
2057-
pushToolResult(
2058-
`The content was successfully inserted in ${relPath.toPosix()}.${newProblemsMessage}`,
2059-
)
2060-
await this.diffViewProvider.reset()
2061-
break
2062-
}
2063-
2064-
const userFeedbackDiff = JSON.stringify({
2065-
tool: "appliedDiff",
2066-
path: getReadablePath(this.cwd, relPath),
2067-
diff: userEdits,
2068-
} satisfies ClineSayTool)
2069-
2070-
console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
2071-
await this.say("user_feedback_diff", userFeedbackDiff)
2072-
pushToolResult(
2073-
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
2074-
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
2075-
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
2076-
`Please note:\n` +
2077-
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
2078-
`2. Proceed with the task using this updated file content as the new baseline.\n` +
2079-
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
2080-
`${newProblemsMessage}`,
2081-
)
2082-
await this.diffViewProvider.reset()
2083-
} catch (error) {
2084-
handleError("insert content", error)
2085-
await this.diffViewProvider.reset()
2086-
}
1943+
insertContentTool(
1944+
this,
1945+
block,
1946+
askApproval,
1947+
handleError,
1948+
pushToolResult,
1949+
removeClosingTag,
1950+
this.diffViewProvider,
1951+
didEditFileEmitter,
1952+
)
20871953
break
20881954
}
20891955

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import * as path from "path"
2+
import * as fs from "fs/promises"
3+
import { Cline } from "../Cline"
4+
import { ClineSayTool } from "../../shared/ExtensionMessage"
5+
import { ToolUse } from "../assistant-message"
6+
import { formatResponse } from "../prompts/responses"
7+
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
8+
import { getReadablePath } from "../../utils/path"
9+
import { fileExistsAtPath } from "../../utils/fs"
10+
import { insertGroups } from "../diff/insert-groups"
11+
import delay from "delay"
12+
import { DiffViewProvider } from "../../integrations/editor/DiffViewProvider"
13+
14+
/**
15+
* Implements the insert_content tool.
16+
*
17+
* @param cline - The instance of Cline that is executing this tool.
18+
* @param block - The block of assistant message content that specifies the
19+
* parameters for this tool.
20+
* @param askApproval - A function that asks the user for approval to show a
21+
* message.
22+
* @param handleError - A function that handles an error that occurred while
23+
* executing this tool.
24+
* @param pushToolResult - A function that pushes the result of this tool to the
25+
* conversation.
26+
* @param removeClosingTag - A function that removes a closing tag from a string.
27+
*/
28+
export async function insertContentTool(
29+
cline: Cline,
30+
block: ToolUse,
31+
askApproval: AskApproval,
32+
handleError: HandleError,
33+
pushToolResult: PushToolResult,
34+
removeClosingTag: RemoveClosingTag,
35+
diffViewProvider: DiffViewProvider,
36+
didEditFileEmitter: (b: boolean) => void, //ensure this is passed by ref
37+
) {
38+
const relPath: string | undefined = block.params.path
39+
const operations: string | undefined = block.params.operations
40+
41+
const sharedMessageProps: ClineSayTool = {
42+
tool: "appliedDiff",
43+
path: getReadablePath(cline.cwd, removeClosingTag("path", relPath)),
44+
}
45+
46+
try {
47+
if (block.partial) {
48+
const partialMessage = JSON.stringify(sharedMessageProps)
49+
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
50+
return
51+
}
52+
53+
// Validate required parameters
54+
if (!relPath) {
55+
cline.consecutiveMistakeCount++
56+
pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "path"))
57+
return
58+
}
59+
60+
if (!operations) {
61+
cline.consecutiveMistakeCount++
62+
pushToolResult(await cline.sayAndCreateMissingParamError("insert_content", "operations"))
63+
return
64+
}
65+
66+
const absolutePath = path.resolve(cline.cwd, relPath)
67+
const fileExists = await fileExistsAtPath(absolutePath)
68+
69+
if (!fileExists) {
70+
cline.consecutiveMistakeCount++
71+
const formattedError = `File does not exist at path: ${absolutePath}\n\n<error_details>\nThe specified file could not be found. Please verify the file path and try again.\n</error_details>`
72+
await cline.say("error", formattedError)
73+
pushToolResult(formattedError)
74+
return
75+
}
76+
77+
let parsedOperations: Array<{
78+
start_line: number
79+
content: string
80+
}>
81+
82+
try {
83+
parsedOperations = JSON.parse(operations)
84+
if (!Array.isArray(parsedOperations)) {
85+
throw new Error("Operations must be an array")
86+
}
87+
} catch (error) {
88+
cline.consecutiveMistakeCount++
89+
await cline.say("error", `Failed to parse operations JSON: ${error.message}`)
90+
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
91+
return
92+
}
93+
94+
cline.consecutiveMistakeCount = 0
95+
96+
// Read the file
97+
const fileContent = await fs.readFile(absolutePath, "utf8")
98+
diffViewProvider.editType = "modify"
99+
diffViewProvider.originalContent = fileContent
100+
const lines = fileContent.split("\n")
101+
102+
const updatedContent = insertGroups(
103+
lines,
104+
parsedOperations.map((elem) => {
105+
return {
106+
index: elem.start_line - 1,
107+
elements: elem.content.split("\n"),
108+
}
109+
}),
110+
).join("\n")
111+
112+
// Show changes in diff view
113+
if (!diffViewProvider.isEditing) {
114+
await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
115+
// First open with original content
116+
await diffViewProvider.open(relPath)
117+
await diffViewProvider.update(fileContent, false)
118+
diffViewProvider.scrollToFirstDiff()
119+
await delay(200)
120+
}
121+
122+
const diff = formatResponse.createPrettyPatch(relPath, fileContent, updatedContent)
123+
124+
if (!diff) {
125+
pushToolResult(`No changes needed for '${relPath}'`)
126+
return
127+
}
128+
129+
await diffViewProvider.update(updatedContent, true)
130+
131+
const completeMessage = JSON.stringify({
132+
...sharedMessageProps,
133+
diff,
134+
} satisfies ClineSayTool)
135+
136+
const didApprove = await askApproval("tool", completeMessage)
137+
138+
if (!didApprove) {
139+
await diffViewProvider.revertChanges()
140+
pushToolResult("Changes were rejected by the user.")
141+
return
142+
}
143+
144+
const { newProblemsMessage, userEdits, finalContent } = await diffViewProvider.saveChanges()
145+
didEditFileEmitter(true)
146+
147+
if (!userEdits) {
148+
pushToolResult(`The content was successfully inserted in ${relPath.toPosix()}.${newProblemsMessage}`)
149+
await diffViewProvider.reset()
150+
return
151+
}
152+
153+
const userFeedbackDiff = JSON.stringify({
154+
tool: "appliedDiff",
155+
path: getReadablePath(cline.cwd, relPath),
156+
diff: userEdits,
157+
} satisfies ClineSayTool)
158+
159+
console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
160+
await cline.say("user_feedback_diff", userFeedbackDiff)
161+
pushToolResult(
162+
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
163+
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file:\n\n` +
164+
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
165+
`Please note:\n` +
166+
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
167+
`2. Proceed with the task using this updated file content as the new baseline.\n` +
168+
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
169+
`${newProblemsMessage}`,
170+
)
171+
await diffViewProvider.reset()
172+
} catch (error) {
173+
await handleError("insert content", error)
174+
await diffViewProvider.reset()
175+
}
176+
}

0 commit comments

Comments
 (0)