Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 8 additions & 16 deletions src/core/tools/__tests__/writeToFileTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { fileExistsAtPath } from "../../../utils/fs"
import { detectCodeOmission } from "../../../integrations/editor/detect-omission"
import { isPathOutsideWorkspace } from "../../../utils/pathUtils"
import { getReadablePath } from "../../../utils/path"
import { unescapeHtmlEntities } from "../../../utils/text-normalization"
import { everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
import { ToolUse, ToolResponse } from "../../../shared/tools"
import { writeToFileTool } from "../writeToFileTool"
Expand Down Expand Up @@ -54,10 +53,6 @@ vi.mock("../../../utils/path", () => ({
getReadablePath: vi.fn().mockReturnValue("test/path.txt"),
}))

vi.mock("../../../utils/text-normalization", () => ({
unescapeHtmlEntities: vi.fn().mockImplementation((content) => content),
}))

vi.mock("../../../integrations/misc/extract-text", () => ({
everyLineHasLineNumbers: vi.fn().mockReturnValue(false),
stripLineNumbers: vi.fn().mockImplementation((content) => content),
Expand Down Expand Up @@ -104,7 +99,6 @@ describe("writeToFileTool", () => {
const mockedDetectCodeOmission = detectCodeOmission as MockedFunction<typeof detectCodeOmission>
const mockedIsPathOutsideWorkspace = isPathOutsideWorkspace as MockedFunction<typeof isPathOutsideWorkspace>
const mockedGetReadablePath = getReadablePath as MockedFunction<typeof getReadablePath>
const mockedUnescapeHtmlEntities = unescapeHtmlEntities as MockedFunction<typeof unescapeHtmlEntities>
const mockedEveryLineHasLineNumbers = everyLineHasLineNumbers as MockedFunction<typeof everyLineHasLineNumbers>
const mockedStripLineNumbers = stripLineNumbers as MockedFunction<typeof stripLineNumbers>
const mockedPathResolve = path.resolve as MockedFunction<typeof path.resolve>
Expand All @@ -124,7 +118,6 @@ describe("writeToFileTool", () => {
mockedDetectCodeOmission.mockReturnValue(false)
mockedIsPathOutsideWorkspace.mockReturnValue(false)
mockedGetReadablePath.mockReturnValue("test/path.txt")
mockedUnescapeHtmlEntities.mockImplementation((content) => content)
mockedEveryLineHasLineNumbers.mockReturnValue(false)
mockedStripLineNumbers.mockImplementation((content) => content)

Expand Down Expand Up @@ -288,20 +281,19 @@ describe("writeToFileTool", () => {
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true)
})

it("unescapes HTML entities for non-Claude models", async () => {
it("preserves HTML entities for all models", async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a test case that specifically reproduces the original issue from #8069? The current test verifies that entities are preserved, but it would be helpful to have a test that demonstrates the actual bug scenario (e.g., applying a diff with < that was incorrectly becoming <).

// Test with non-Claude model
mockCline.api.getModel.mockReturnValue({ id: "gpt-4" })

await executeWriteFileTool({ content: "&lt;test&gt;" })
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("&lt;test&gt;", true)

expect(mockedUnescapeHtmlEntities).toHaveBeenCalledWith("&lt;test&gt;")
})
// Reset mocks
mockCline.diffViewProvider.update.mockClear()

it("skips HTML unescaping for Claude models", async () => {
// Test with Claude model
mockCline.api.getModel.mockReturnValue({ id: "claude-3" })

await executeWriteFileTool({ content: "&lt;test&gt;" })

expect(mockedUnescapeHtmlEntities).not.toHaveBeenCalled()
await executeWriteFileTool({ content: "&amp;hello&quot;" })
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("&amp;hello&quot;", true)
})

it("strips line numbers from numbered content", async () => {
Expand Down
6 changes: 2 additions & 4 deletions src/core/tools/applyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } f
import { formatResponse } from "../prompts/responses"
import { fileExistsAtPath } from "../../utils/fs"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"

export async function applyDiffToolLegacy(
Expand All @@ -25,9 +24,8 @@ export async function applyDiffToolLegacy(
const relPath: string | undefined = block.params.path
let diffContent: string | undefined = block.params.diff

if (diffContent && !cline.api.getModel().id.includes("claude")) {
diffContent = unescapeHtmlEntities(diffContent)
}
// HTML entities should be preserved exactly as provided
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good change to preserve HTML entities. However, I notice that executeCommandTool.ts still uses unescapeHtmlEntities. Is this intentional? Shell commands might need actual characters like < and > for redirection, but it would be good to document why command execution needs different handling than file operations.

// to ensure accurate diff matching and prevent unintended changes

const sharedMessageProps: ClineSayTool = {
tool: "appliedDiff",
Expand Down
11 changes: 3 additions & 8 deletions src/core/tools/multiApplyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } f
import { formatResponse } from "../prompts/responses"
import { fileExistsAtPath } from "../../utils/fs"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { parseXmlForDiff } from "../../utils/xml"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { applyDiffToolLegacy } from "./applyDiffTool"
Expand Down Expand Up @@ -421,13 +420,9 @@ Original error: ${errorMessage}`
let successCount = 0
let formattedError = ""

// Pre-process all diff items for HTML entity unescaping if needed
const processedDiffItems = !cline.api.getModel().id.includes("claude")
? diffItems.map((item) => ({
...item,
content: item.content ? unescapeHtmlEntities(item.content) : item.content,
}))
: diffItems
// HTML entities should be preserved exactly as provided
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment style here is slightly different from the one in applyDiffTool.ts (2 lines vs 3 lines). Consider using consistent formatting across files for better readability.

// to ensure accurate diff matching and prevent unintended changes
const processedDiffItems = diffItems

// Apply all diffs at once with the array-based method
const diffResult = (await cline.diffStrategy?.applyDiff(originalContent, processedDiffItems)) ?? {
Expand Down
6 changes: 2 additions & 4 deletions src/core/tools/writeToFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/mi
import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { detectCodeOmission } from "../../integrations/editor/detect-omission"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"

Expand Down Expand Up @@ -83,9 +82,8 @@ export async function writeToFileTool(
newContent = newContent.split("\n").slice(0, -1).join("\n")
}

if (!cline.api.getModel().id.includes("claude")) {
newContent = unescapeHtmlEntities(newContent)
}
// HTML entities should be preserved exactly as provided
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to see HTML entities being preserved here as well. This ensures consistency across all file write operations.

// to ensure content is written exactly as intended

// Determine if the path is outside the workspace
const fullPath = relPath ? path.resolve(cline.cwd, removeClosingTag("path", relPath)) : ""
Expand Down
Loading