Skip to content

Commit 0a0ff43

Browse files
mrubenssamhvw8
authored andcommitted
Checkpoint on insert and search/replace tools
1 parent f47dd2d commit 0a0ff43

File tree

7 files changed

+459
-1
lines changed

7 files changed

+459
-1
lines changed

src/core/Cline.ts

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import { BrowserSession } from "../services/browser/BrowserSession"
6060
import { OpenRouterHandler } from "../api/providers/openrouter"
6161
import { McpHub } from "../services/mcp/McpHub"
6262
import crypto from "crypto"
63+
import { insertGroups } from "./diff/insert-groups"
6364

6465
const cwd =
6566
vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -1008,6 +1009,10 @@ export class Cline {
10081009
return `[${block.name} for '${block.params.regex}'${
10091010
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
10101011
}]`
1012+
case "insert_code_block":
1013+
return `[${block.name} for '${block.params.path}']`
1014+
case "search_and_replace":
1015+
return `[${block.name} for '${block.params.path}']`
10111016
case "list_files":
10121017
return `[${block.name} for '${block.params.path}']`
10131018
case "list_code_definition_names":
@@ -1479,6 +1484,323 @@ export class Cline {
14791484
break
14801485
}
14811486
}
1487+
1488+
case "insert_code_block": {
1489+
const relPath: string | undefined = block.params.path
1490+
const operations: string | undefined = block.params.operations
1491+
1492+
const sharedMessageProps: ClineSayTool = {
1493+
tool: "appliedDiff",
1494+
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
1495+
}
1496+
1497+
try {
1498+
if (block.partial) {
1499+
const partialMessage = JSON.stringify(sharedMessageProps)
1500+
await this.ask("tool", partialMessage, block.partial).catch(() => {})
1501+
break
1502+
}
1503+
1504+
// Validate required parameters
1505+
if (!relPath) {
1506+
this.consecutiveMistakeCount++
1507+
pushToolResult(await this.sayAndCreateMissingParamError("insert_code_block", "path"))
1508+
break
1509+
}
1510+
1511+
if (!operations) {
1512+
this.consecutiveMistakeCount++
1513+
pushToolResult(
1514+
await this.sayAndCreateMissingParamError("insert_code_block", "operations"),
1515+
)
1516+
break
1517+
}
1518+
1519+
const absolutePath = path.resolve(cwd, relPath)
1520+
const fileExists = await fileExistsAtPath(absolutePath)
1521+
1522+
if (!fileExists) {
1523+
this.consecutiveMistakeCount++
1524+
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>`
1525+
await this.say("error", formattedError)
1526+
pushToolResult(formattedError)
1527+
break
1528+
}
1529+
1530+
let parsedOperations: Array<{
1531+
start_line: number
1532+
content: string
1533+
}>
1534+
1535+
try {
1536+
parsedOperations = JSON.parse(operations)
1537+
if (!Array.isArray(parsedOperations)) {
1538+
throw new Error("Operations must be an array")
1539+
}
1540+
} catch (error) {
1541+
this.consecutiveMistakeCount++
1542+
await this.say("error", `Failed to parse operations JSON: ${error.message}`)
1543+
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
1544+
break
1545+
}
1546+
1547+
this.consecutiveMistakeCount = 0
1548+
1549+
// Read the file
1550+
const fileContent = await fs.readFile(absolutePath, "utf8")
1551+
this.diffViewProvider.editType = "modify"
1552+
this.diffViewProvider.originalContent = fileContent
1553+
const lines = fileContent.split("\n")
1554+
1555+
const updatedContent = insertGroups(
1556+
lines,
1557+
parsedOperations.map((elem) => {
1558+
return {
1559+
index: elem.start_line - 1,
1560+
elements: elem.content.split("\n"),
1561+
}
1562+
}),
1563+
).join("\n")
1564+
1565+
// Show changes in diff view
1566+
if (!this.diffViewProvider.isEditing) {
1567+
await this.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
1568+
// First open with original content
1569+
await this.diffViewProvider.open(relPath)
1570+
await this.diffViewProvider.update(fileContent, false)
1571+
this.diffViewProvider.scrollToFirstDiff()
1572+
await delay(200)
1573+
}
1574+
1575+
const diff = formatResponse.createPrettyPatch(
1576+
relPath,
1577+
this.diffViewProvider.originalContent,
1578+
updatedContent,
1579+
)
1580+
1581+
if (!diff) {
1582+
pushToolResult(`No changes needed for '${relPath}'`)
1583+
break
1584+
}
1585+
1586+
await this.diffViewProvider.update(updatedContent, true)
1587+
1588+
const completeMessage = JSON.stringify({
1589+
...sharedMessageProps,
1590+
diff,
1591+
} satisfies ClineSayTool)
1592+
1593+
const didApprove = await this.ask("tool", completeMessage, false).then(
1594+
(response) => response.response === "yesButtonClicked",
1595+
)
1596+
1597+
if (!didApprove) {
1598+
await this.diffViewProvider.revertChanges()
1599+
pushToolResult("Changes were rejected by the user.")
1600+
break
1601+
}
1602+
1603+
const { newProblemsMessage, userEdits, finalContent } =
1604+
await this.diffViewProvider.saveChanges()
1605+
this.didEditFile = true
1606+
1607+
if (!userEdits) {
1608+
pushToolResult(
1609+
`The code block was successfully inserted in ${relPath.toPosix()}.${newProblemsMessage}`,
1610+
)
1611+
await this.diffViewProvider.reset()
1612+
break
1613+
}
1614+
1615+
const userFeedbackDiff = JSON.stringify({
1616+
tool: "appliedDiff",
1617+
path: getReadablePath(cwd, relPath),
1618+
diff: userEdits,
1619+
} satisfies ClineSayTool)
1620+
1621+
console.debug("[DEBUG] User made edits, sending feedback diff:", userFeedbackDiff)
1622+
await this.say("user_feedback_diff", userFeedbackDiff)
1623+
pushToolResult(
1624+
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1625+
`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` +
1626+
`<final_file_content path="${relPath.toPosix()}">\n${finalContent}\n</final_file_content>\n\n` +
1627+
`Please note:\n` +
1628+
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
1629+
`2. Proceed with the task using this updated file content as the new baseline.\n` +
1630+
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
1631+
`${newProblemsMessage}`,
1632+
)
1633+
await this.diffViewProvider.reset()
1634+
} catch (error) {
1635+
handleError("insert block", error)
1636+
await this.diffViewProvider.reset()
1637+
}
1638+
break
1639+
}
1640+
1641+
case "search_and_replace": {
1642+
const relPath: string | undefined = block.params.path
1643+
const operations: string | undefined = block.params.operations
1644+
1645+
const sharedMessageProps: ClineSayTool = {
1646+
tool: "appliedDiff",
1647+
path: getReadablePath(cwd, removeClosingTag("path", relPath)),
1648+
}
1649+
1650+
try {
1651+
if (block.partial) {
1652+
const partialMessage = JSON.stringify({
1653+
path: removeClosingTag("path", relPath),
1654+
operations: removeClosingTag("operations", operations),
1655+
})
1656+
await this.ask("tool", partialMessage, block.partial).catch(() => {})
1657+
break
1658+
} else {
1659+
if (!relPath) {
1660+
this.consecutiveMistakeCount++
1661+
pushToolResult(
1662+
await this.sayAndCreateMissingParamError("search_and_replace", "path"),
1663+
)
1664+
break
1665+
}
1666+
if (!operations) {
1667+
this.consecutiveMistakeCount++
1668+
pushToolResult(
1669+
await this.sayAndCreateMissingParamError("search_and_replace", "operations"),
1670+
)
1671+
break
1672+
}
1673+
1674+
const absolutePath = path.resolve(cwd, relPath)
1675+
const fileExists = await fileExistsAtPath(absolutePath)
1676+
1677+
if (!fileExists) {
1678+
this.consecutiveMistakeCount++
1679+
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>`
1680+
await this.say("error", formattedError)
1681+
pushToolResult(formattedError)
1682+
break
1683+
}
1684+
1685+
let parsedOperations: Array<{
1686+
search: string
1687+
replace: string
1688+
start_line?: number
1689+
end_line?: number
1690+
use_regex?: boolean
1691+
ignore_case?: boolean
1692+
regex_flags?: string
1693+
}>
1694+
1695+
try {
1696+
parsedOperations = JSON.parse(operations)
1697+
if (!Array.isArray(parsedOperations)) {
1698+
throw new Error("Operations must be an array")
1699+
}
1700+
} catch (error) {
1701+
this.consecutiveMistakeCount++
1702+
await this.say("error", `Failed to parse operations JSON: ${error.message}`)
1703+
pushToolResult(formatResponse.toolError("Invalid operations JSON format"))
1704+
break
1705+
}
1706+
1707+
// Read the original file content
1708+
const fileContent = await fs.readFile(absolutePath, "utf-8")
1709+
const lines = fileContent.split("\n")
1710+
let newContent = fileContent
1711+
1712+
// Apply each search/replace operation
1713+
for (const op of parsedOperations) {
1714+
const searchPattern = op.use_regex
1715+
? new RegExp(op.search, op.regex_flags || (op.ignore_case ? "gi" : "g"))
1716+
: new RegExp(escapeRegExp(op.search), op.ignore_case ? "gi" : "g")
1717+
1718+
if (op.start_line || op.end_line) {
1719+
// Line-restricted replacement
1720+
const startLine = (op.start_line || 1) - 1
1721+
const endLine = (op.end_line || lines.length) - 1
1722+
1723+
const beforeLines = lines.slice(0, startLine)
1724+
const targetLines = lines.slice(startLine, endLine + 1)
1725+
const afterLines = lines.slice(endLine + 1)
1726+
1727+
const modifiedLines = targetLines.map((line) =>
1728+
line.replace(searchPattern, op.replace),
1729+
)
1730+
1731+
newContent = [...beforeLines, ...modifiedLines, ...afterLines].join("\n")
1732+
} else {
1733+
// Global replacement
1734+
newContent = newContent.replace(searchPattern, op.replace)
1735+
}
1736+
}
1737+
1738+
this.consecutiveMistakeCount = 0
1739+
1740+
// Show diff preview
1741+
const diff = formatResponse.createPrettyPatch(
1742+
relPath,
1743+
this.diffViewProvider.originalContent,
1744+
newContent,
1745+
)
1746+
1747+
if (!diff) {
1748+
pushToolResult(`No changes needed for '${relPath}'`)
1749+
break
1750+
}
1751+
1752+
await this.diffViewProvider.open(relPath)
1753+
await this.diffViewProvider.update(newContent, true)
1754+
this.diffViewProvider.scrollToFirstDiff()
1755+
1756+
const completeMessage = JSON.stringify({
1757+
...sharedMessageProps,
1758+
diff: diff,
1759+
} satisfies ClineSayTool)
1760+
1761+
const didApprove = await askApproval("tool", completeMessage)
1762+
if (!didApprove) {
1763+
await this.diffViewProvider.revertChanges() // This likely handles closing the diff view
1764+
break
1765+
}
1766+
1767+
const { newProblemsMessage, userEdits, finalContent } =
1768+
await this.diffViewProvider.saveChanges()
1769+
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1770+
if (userEdits) {
1771+
await this.say(
1772+
"user_feedback_diff",
1773+
JSON.stringify({
1774+
tool: fileExists ? "editedExistingFile" : "newFileCreated",
1775+
path: getReadablePath(cwd, relPath),
1776+
diff: userEdits,
1777+
} satisfies ClineSayTool),
1778+
)
1779+
pushToolResult(
1780+
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1781+
`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, including line numbers:\n\n` +
1782+
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(finalContent || "")}\n</final_file_content>\n\n` +
1783+
`Please note:\n` +
1784+
`1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
1785+
`2. Proceed with the task using this updated file content as the new baseline.\n` +
1786+
`3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
1787+
`${newProblemsMessage}`,
1788+
)
1789+
} else {
1790+
pushToolResult(
1791+
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
1792+
)
1793+
}
1794+
await this.diffViewProvider.reset()
1795+
break
1796+
}
1797+
} catch (error) {
1798+
await handleError("applying search and replace", error)
1799+
await this.diffViewProvider.reset()
1800+
break
1801+
}
1802+
}
1803+
14821804
case "read_file": {
14831805
const relPath: string | undefined = block.params.path
14841806
const sharedMessageProps: ClineSayTool = {
@@ -2746,3 +3068,7 @@ export class Cline {
27463068
return `<environment_details>\n${details.trim()}\n</environment_details>`
27473069
}
27483070
}
3071+
3072+
function escapeRegExp(string: string): string {
3073+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
3074+
}

src/core/assistant-message/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const toolUseNames = [
1313
"read_file",
1414
"write_to_file",
1515
"apply_diff",
16+
"insert_code_block",
17+
"search_and_replace",
1618
"search_files",
1719
"list_files",
1820
"list_code_definition_names",
@@ -50,6 +52,7 @@ export const toolParamNames = [
5052
"end_line",
5153
"mode_slug",
5254
"reason",
55+
"operations",
5356
] as const
5457

5558
export type ToolParamName = (typeof toolParamNames)[number]
@@ -78,6 +81,11 @@ export interface WriteToFileToolUse extends ToolUse {
7881
params: Partial<Pick<Record<ToolParamName, string>, "path" | "content" | "line_count">>
7982
}
8083

84+
export interface InsertCodeBlockToolUse extends ToolUse {
85+
name: "insert_code_block"
86+
params: Partial<Pick<Record<ToolParamName, string>, "path" | "operations">>
87+
}
88+
8189
export interface SearchFilesToolUse extends ToolUse {
8290
name: "search_files"
8391
params: Partial<Pick<Record<ToolParamName, string>, "path" | "regex" | "file_pattern">>

0 commit comments

Comments
 (0)