@@ -60,6 +60,7 @@ import { BrowserSession } from "../services/browser/BrowserSession"
6060import { OpenRouterHandler } from "../api/providers/openrouter"
6161import { McpHub } from "../services/mcp/McpHub"
6262import crypto from "crypto"
63+ import { insertGroups } from "./diff/insert-groups"
6364
6465const 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+ }
0 commit comments