@@ -85,6 +85,7 @@ import { telemetryService } from "../services/telemetry/TelemetryService"
8585import { validateToolUse , isToolAllowedForMode , ToolName } from "./mode-validator"
8686import { parseXml } from "../utils/xml"
8787import { getWorkspacePath } from "../utils/path"
88+ import { writeToFileTool } from "./tools/writeToFileTool"
8889
8990export type ToolResponse = string | Array < Anthropic . TextBlockParam | Anthropic . ImageBlockParam >
9091type UserContent = Array < Anthropic . Messages . ContentBlockParam >
@@ -135,7 +136,7 @@ export class Cline extends EventEmitter<ClineEvents> {
135136 api : ApiHandler
136137 private urlContentFetcher : UrlContentFetcher
137138 private browserSession : BrowserSession
138- private didEditFile : boolean = false
139+ didEditFile : boolean = false
139140 customInstructions ?: string
140141 diffStrategy ?: DiffStrategy
141142 diffEnabled : boolean = false
@@ -156,7 +157,7 @@ export class Cline extends EventEmitter<ClineEvents> {
156157 private abort : boolean = false
157158 didFinishAbortingStream = false
158159 abandoned = false
159- private diffViewProvider : DiffViewProvider
160+ diffViewProvider : DiffViewProvider
160161 private lastApiRequestTime ?: number
161162 isInitialized = false
162163
@@ -1564,211 +1565,9 @@ export class Cline extends EventEmitter<ClineEvents> {
15641565 }
15651566
15661567 switch ( block . name ) {
1567- case "write_to_file" : {
1568- const relPath : string | undefined = block . params . path
1569- let newContent : string | undefined = block . params . content
1570- let predictedLineCount : number | undefined = parseInt ( block . params . line_count ?? "0" )
1571- if ( ! relPath || ! newContent ) {
1572- // checking for newContent ensure relPath is complete
1573- // wait so we can determine if it's a new file or editing an existing file
1574- break
1575- }
1576-
1577- const accessAllowed = this . rooIgnoreController ?. validateAccess ( relPath )
1578- if ( ! accessAllowed ) {
1579- await this . say ( "rooignore_error" , relPath )
1580- pushToolResult ( formatResponse . toolError ( formatResponse . rooIgnoreError ( relPath ) ) )
1581-
1582- break
1583- }
1584-
1585- // Check if file exists using cached map or fs.access
1586- let fileExists : boolean
1587- if ( this . diffViewProvider . editType !== undefined ) {
1588- fileExists = this . diffViewProvider . editType === "modify"
1589- } else {
1590- const absolutePath = path . resolve ( this . cwd , relPath )
1591- fileExists = await fileExistsAtPath ( absolutePath )
1592- this . diffViewProvider . editType = fileExists ? "modify" : "create"
1593- }
1594-
1595- // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini)
1596- if ( newContent . startsWith ( "```" ) ) {
1597- // this handles cases where it includes language specifiers like ```python ```js
1598- newContent = newContent . split ( "\n" ) . slice ( 1 ) . join ( "\n" ) . trim ( )
1599- }
1600- if ( newContent . endsWith ( "```" ) ) {
1601- newContent = newContent . split ( "\n" ) . slice ( 0 , - 1 ) . join ( "\n" ) . trim ( )
1602- }
1603-
1604- if ( ! this . api . getModel ( ) . id . includes ( "claude" ) ) {
1605- // it seems not just llama models are doing this, but also gemini and potentially others
1606- if (
1607- newContent . includes ( ">" ) ||
1608- newContent . includes ( "<" ) ||
1609- newContent . includes ( """ )
1610- ) {
1611- newContent = newContent
1612- . replace ( / & g t ; / g, ">" )
1613- . replace ( / & l t ; / g, "<" )
1614- . replace ( / & q u o t ; / g, '"' )
1615- }
1616- }
1617-
1618- // Determine if the path is outside the workspace
1619- const fullPath = relPath ? path . resolve ( this . cwd , removeClosingTag ( "path" , relPath ) ) : ""
1620- const isOutsideWorkspace = isPathOutsideWorkspace ( fullPath )
1621-
1622- const sharedMessageProps : ClineSayTool = {
1623- tool : fileExists ? "editedExistingFile" : "newFileCreated" ,
1624- path : getReadablePath ( this . cwd , removeClosingTag ( "path" , relPath ) ) ,
1625- isOutsideWorkspace,
1626- }
1627- try {
1628- if ( block . partial ) {
1629- // update gui message
1630- const partialMessage = JSON . stringify ( sharedMessageProps )
1631- await this . ask ( "tool" , partialMessage , block . partial ) . catch ( ( ) => { } )
1632- // update editor
1633- if ( ! this . diffViewProvider . isEditing ) {
1634- // open the editor and prepare to stream content in
1635- await this . diffViewProvider . open ( relPath )
1636- }
1637- // editor is open, stream content in
1638- await this . diffViewProvider . update (
1639- everyLineHasLineNumbers ( newContent ) ? stripLineNumbers ( newContent ) : newContent ,
1640- false ,
1641- )
1642- break
1643- } else {
1644- if ( ! relPath ) {
1645- this . consecutiveMistakeCount ++
1646- pushToolResult ( await this . sayAndCreateMissingParamError ( "write_to_file" , "path" ) )
1647- await this . diffViewProvider . reset ( )
1648- break
1649- }
1650- if ( ! newContent ) {
1651- this . consecutiveMistakeCount ++
1652- pushToolResult ( await this . sayAndCreateMissingParamError ( "write_to_file" , "content" ) )
1653- await this . diffViewProvider . reset ( )
1654- break
1655- }
1656- if ( ! predictedLineCount ) {
1657- this . consecutiveMistakeCount ++
1658- pushToolResult (
1659- await this . sayAndCreateMissingParamError ( "write_to_file" , "line_count" ) ,
1660- )
1661- await this . diffViewProvider . reset ( )
1662- break
1663- }
1664- this . consecutiveMistakeCount = 0
1665-
1666- // if isEditingFile false, that means we have the full contents of the file already.
1667- // it's important to note how this function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So this part of the logic will always be called.
1668- // in other words, you must always repeat the block.partial logic here
1669- if ( ! this . diffViewProvider . isEditing ) {
1670- // show gui message before showing edit animation
1671- const partialMessage = JSON . stringify ( sharedMessageProps )
1672- await this . ask ( "tool" , partialMessage , true ) . catch ( ( ) => { } ) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
1673- await this . diffViewProvider . open ( relPath )
1674- }
1675- await this . diffViewProvider . update (
1676- everyLineHasLineNumbers ( newContent ) ? stripLineNumbers ( newContent ) : newContent ,
1677- true ,
1678- )
1679- await delay ( 300 ) // wait for diff view to update
1680- this . diffViewProvider . scrollToFirstDiff ( )
1681-
1682- // Check for code omissions before proceeding
1683- if (
1684- detectCodeOmission (
1685- this . diffViewProvider . originalContent || "" ,
1686- newContent ,
1687- predictedLineCount ,
1688- )
1689- ) {
1690- if ( this . diffStrategy ) {
1691- await this . diffViewProvider . revertChanges ( )
1692- pushToolResult (
1693- formatResponse . toolError (
1694- `Content appears to be truncated (file has ${
1695- newContent . split ( "\n" ) . length
1696- } lines but was predicted to have ${ predictedLineCount } lines), and found comments indicating omitted code (e.g., '// rest of code unchanged', '/* previous code */'). Please provide the complete file content without any omissions if possible, or otherwise use the 'apply_diff' tool to apply the diff to the original file.`,
1697- ) ,
1698- )
1699- break
1700- } else {
1701- vscode . window
1702- . showWarningMessage (
1703- "Potential code truncation detected. This happens when the AI reaches its max output limit." ,
1704- "Follow this guide to fix the issue" ,
1705- )
1706- . then ( ( selection ) => {
1707- if ( selection === "Follow this guide to fix the issue" ) {
1708- vscode . env . openExternal (
1709- vscode . Uri . parse (
1710- "https://github.com/cline/cline/wiki/Troubleshooting-%E2%80%90-Cline-Deleting-Code-with-%22Rest-of-Code-Here%22-Comments" ,
1711- ) ,
1712- )
1713- }
1714- } )
1715- }
1716- }
1717-
1718- const completeMessage = JSON . stringify ( {
1719- ...sharedMessageProps ,
1720- content : fileExists ? undefined : newContent ,
1721- diff : fileExists
1722- ? formatResponse . createPrettyPatch (
1723- relPath ,
1724- this . diffViewProvider . originalContent ,
1725- newContent ,
1726- )
1727- : undefined ,
1728- } satisfies ClineSayTool )
1729- const didApprove = await askApproval ( "tool" , completeMessage )
1730- if ( ! didApprove ) {
1731- await this . diffViewProvider . revertChanges ( )
1732- break
1733- }
1734- const { newProblemsMessage, userEdits, finalContent } =
1735- await this . diffViewProvider . saveChanges ( )
1736- this . didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1737- if ( userEdits ) {
1738- await this . say (
1739- "user_feedback_diff" ,
1740- JSON . stringify ( {
1741- tool : fileExists ? "editedExistingFile" : "newFileCreated" ,
1742- path : getReadablePath ( this . cwd , relPath ) ,
1743- diff : userEdits ,
1744- } satisfies ClineSayTool ) ,
1745- )
1746- pushToolResult (
1747- `The user made the following updates to your content:\n\n${ userEdits } \n\n` +
1748- `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` +
1749- `<final_file_content path="${ relPath . toPosix ( ) } ">\n${ addLineNumbers (
1750- finalContent || "" ,
1751- ) } \n</final_file_content>\n\n` +
1752- `Please note:\n` +
1753- `1. You do not need to re-write the file with these changes, as they have already been applied.\n` +
1754- `2. Proceed with the task using this updated file content as the new baseline.\n` +
1755- `3. If the user's edits have addressed part of the task or changed the requirements, adjust your approach accordingly.` +
1756- `${ newProblemsMessage } ` ,
1757- )
1758- } else {
1759- pushToolResult (
1760- `The content was successfully saved to ${ relPath . toPosix ( ) } .${ newProblemsMessage } ` ,
1761- )
1762- }
1763- await this . diffViewProvider . reset ( )
1764- break
1765- }
1766- } catch ( error ) {
1767- await handleError ( "writing file" , error )
1768- await this . diffViewProvider . reset ( )
1769- break
1770- }
1771- }
1568+ case "write_to_file" :
1569+ await writeToFileTool ( this , block , askApproval , handleError , pushToolResult , removeClosingTag )
1570+ break
17721571 case "apply_diff" : {
17731572 const relPath : string | undefined = block . params . path
17741573 const diffContent : string | undefined = block . params . diff
0 commit comments