Skip to content

Commit 26c3fe6

Browse files
authored
Move write_to_file to a tool file (#2093)
1 parent 1242ba8 commit 26c3fe6

File tree

2 files changed

+215
-207
lines changed

2 files changed

+215
-207
lines changed

src/core/Cline.ts

Lines changed: 6 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { telemetryService } from "../services/telemetry/TelemetryService"
8585
import { validateToolUse, isToolAllowedForMode, ToolName } from "./mode-validator"
8686
import { parseXml } from "../utils/xml"
8787
import { getWorkspacePath } from "../utils/path"
88+
import { writeToFileTool } from "./tools/writeToFileTool"
8889

8990
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
9091
type 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("&gt;") ||
1608-
newContent.includes("&lt;") ||
1609-
newContent.includes("&quot;")
1610-
) {
1611-
newContent = newContent
1612-
.replace(/&gt;/g, ">")
1613-
.replace(/&lt;/g, "<")
1614-
.replace(/&quot;/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

Comments
 (0)