Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 9fb232b

Browse files
committed
fix: remove HTML entity unescaping from tool content processing
Removes unescapeHtmlEntities() calls from ApplyDiffTool, WriteToFileTool, and ExecuteCommandTool. Since all tool calls now use native JSON parsing, HTML entities in parsed content are intentional (e.g. Go code doing HTML escaping) rather than encoding artifacts. The lossy unescaping was causing apply_diff failures and file content corruption when working with code that legitimately contains HTML entities. Fixes #12264
1 parent ad25634 commit 9fb232b

5 files changed

Lines changed: 6 additions & 68 deletions

File tree

src/core/tools/ApplyDiffTool.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Task } from "../task/Task"
99
import { formatResponse } from "../prompts/responses"
1010
import { fileExistsAtPath } from "../../utils/fs"
1111
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
12-
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1312
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1413
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1514
import type { ToolUse } from "../../shared/tools"
@@ -28,10 +27,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
2827
const { askApproval, handleError, pushToolResult } = callbacks
2928
let { path: relPath, diff: diffContent } = params
3029

31-
if (diffContent && !task.api.getModel().id.includes("claude")) {
32-
diffContent = unescapeHtmlEntities(diffContent)
33-
}
34-
3530
try {
3631
if (!relPath) {
3732
task.consecutiveMistakeCount++

src/core/tools/ExecuteCommandTool.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { Task } from "../task/Task"
1111

1212
import { ToolUse, ToolResponse } from "../../shared/tools"
1313
import { formatResponse } from "../prompts/responses"
14-
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1514
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
1615
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1716
import { Terminal } from "../../integrations/terminal/Terminal"
@@ -53,9 +52,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
5352
return
5453
}
5554

56-
const canonicalCommand = unescapeHtmlEntities(command)
57-
58-
const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(canonicalCommand)
55+
const ignoredFileAttemptedToAccess = task.rooIgnoreController?.validateCommand(command)
5956

6057
if (ignoredFileAttemptedToAccess) {
6158
await task.say("rooignore_error", ignoredFileAttemptedToAccess)
@@ -65,7 +62,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
6562

6663
task.consecutiveMistakeCount = 0
6764

68-
const didApprove = await askApproval("command", canonicalCommand)
65+
const didApprove = await askApproval("command", command)
6966

7067
if (!didApprove) {
7168
return
@@ -88,9 +85,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
8885
.get<string[]>("commandTimeoutAllowlist", [])
8986

9087
// Check if command matches any prefix in the allowlist
91-
const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) =>
92-
canonicalCommand.startsWith(prefix.trim()),
93-
)
88+
const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) => command.startsWith(prefix.trim()))
9489

9590
// Convert seconds to milliseconds for internal use, but skip timeout if command is allowlisted
9691
const commandExecutionTimeout = isCommandAllowlisted ? 0 : commandExecutionTimeoutSeconds * 1000
@@ -100,7 +95,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
10095

10196
const options: ExecuteCommandOptions = {
10297
executionId,
103-
command: canonicalCommand,
98+
command: command,
10499
customCwd,
105100
terminalShellIntegrationDisabled,
106101
commandExecutionTimeout,

src/core/tools/WriteToFileTool.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { fileExistsAtPath, createDirectoriesForFile } from "../../utils/fs"
1111
import { stripLineNumbers, everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
1212
import { getReadablePath } from "../../utils/path"
1313
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
14-
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1514
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1615
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1716
import type { ToolUse } from "../../shared/tools"
@@ -81,10 +80,6 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
8180
newContent = newContent.split("\n").slice(0, -1).join("\n")
8281
}
8382

84-
if (!task.api.getModel().id.includes("claude")) {
85-
newContent = unescapeHtmlEntities(newContent)
86-
}
87-
8883
const fullPath = relPath ? path.resolve(task.cwd, relPath) : ""
8984
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
9085

src/core/tools/__tests__/executeCommandTool.spec.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import * as vscode from "vscode"
66
import { Task } from "../../task/Task"
77
import { formatResponse } from "../../prompts/responses"
88
import { ToolUse, AskApproval, HandleError, PushToolResult } from "../../../shared/tools"
9-
import { unescapeHtmlEntities } from "../../../utils/text-normalization"
109

1110
// Mock dependencies
1211
vitest.mock("execa", () => ({
@@ -111,37 +110,6 @@ describe("executeCommandTool", () => {
111110
process.env.ROO_CLI_RUNTIME = originalCliRuntime
112111
})
113112

114-
/**
115-
* Tests for HTML entity unescaping in commands
116-
* This verifies that HTML entities are properly converted to their actual characters
117-
*/
118-
describe("HTML entity unescaping", () => {
119-
it("should unescape &lt; to < character", () => {
120-
const input = "echo &lt;test&gt;"
121-
const expected = "echo <test>"
122-
expect(unescapeHtmlEntities(input)).toBe(expected)
123-
})
124-
125-
it("should unescape &gt; to > character", () => {
126-
const input = "echo test &gt; output.txt"
127-
const expected = "echo test > output.txt"
128-
expect(unescapeHtmlEntities(input)).toBe(expected)
129-
})
130-
131-
it("should unescape &amp; to & character", () => {
132-
const input = "echo foo &amp;&amp; echo bar"
133-
const expected = "echo foo && echo bar"
134-
expect(unescapeHtmlEntities(input)).toBe(expected)
135-
})
136-
137-
it("should handle multiple mixed HTML entities", () => {
138-
const input = "grep -E 'pattern' &lt;file.txt &gt;output.txt 2&gt;&amp;1"
139-
const expected = "grep -E 'pattern' <file.txt >output.txt 2>&1"
140-
expect(unescapeHtmlEntities(input)).toBe(expected)
141-
})
142-
})
143-
144-
// Now we can run these tests
145113
describe("Basic functionality", () => {
146114
it("should execute a command normally", async () => {
147115
// Setup

src/core/tools/__tests__/writeToFileTool.spec.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { MockedFunction } from "vitest"
55
import { fileExistsAtPath, createDirectoriesForFile } from "../../../utils/fs"
66
import { isPathOutsideWorkspace } from "../../../utils/pathUtils"
77
import { getReadablePath } from "../../../utils/path"
8-
import { unescapeHtmlEntities } from "../../../utils/text-normalization"
98
import { everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
109
import { ToolUse, ToolResponse } from "../../../shared/tools"
1110
import { writeToFileTool } from "../WriteToFileTool"
@@ -47,10 +46,6 @@ vi.mock("../../../utils/path", () => ({
4746
getReadablePath: vi.fn().mockReturnValue("test/path.txt"),
4847
}))
4948

50-
vi.mock("../../../utils/text-normalization", () => ({
51-
unescapeHtmlEntities: vi.fn().mockImplementation((content) => content),
52-
}))
53-
5449
vi.mock("../../../integrations/misc/extract-text", () => ({
5550
everyLineHasLineNumbers: vi.fn().mockReturnValue(false),
5651
stripLineNumbers: vi.fn().mockImplementation((content) => content),
@@ -97,7 +92,6 @@ describe("writeToFileTool", () => {
9792
const mockedCreateDirectoriesForFile = createDirectoriesForFile as MockedFunction<typeof createDirectoriesForFile>
9893
const mockedIsPathOutsideWorkspace = isPathOutsideWorkspace as MockedFunction<typeof isPathOutsideWorkspace>
9994
const mockedGetReadablePath = getReadablePath as MockedFunction<typeof getReadablePath>
100-
const mockedUnescapeHtmlEntities = unescapeHtmlEntities as MockedFunction<typeof unescapeHtmlEntities>
10195
const mockedEveryLineHasLineNumbers = everyLineHasLineNumbers as MockedFunction<typeof everyLineHasLineNumbers>
10296
const mockedStripLineNumbers = stripLineNumbers as MockedFunction<typeof stripLineNumbers>
10397
const mockedPathResolve = path.resolve as MockedFunction<typeof path.resolve>
@@ -116,7 +110,6 @@ describe("writeToFileTool", () => {
116110
mockedFileExistsAtPath.mockResolvedValue(false)
117111
mockedIsPathOutsideWorkspace.mockReturnValue(false)
118112
mockedGetReadablePath.mockReturnValue("test/path.txt")
119-
mockedUnescapeHtmlEntities.mockImplementation((content) => content)
120113
mockedEveryLineHasLineNumbers.mockReturnValue(false)
121114
mockedStripLineNumbers.mockImplementation((content) => content)
122115

@@ -327,20 +320,12 @@ describe("writeToFileTool", () => {
327320
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true)
328321
})
329322

330-
it("unescapes HTML entities for non-Claude models", async () => {
323+
it("preserves HTML entities in content (no unescaping)", async () => {
331324
mockCline.api.getModel.mockReturnValue({ id: "gpt-4" })
332325

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

335-
expect(mockedUnescapeHtmlEntities).toHaveBeenCalledWith("&lt;test&gt;")
336-
})
337-
338-
it("skips HTML unescaping for Claude models", async () => {
339-
mockCline.api.getModel.mockReturnValue({ id: "claude-3" })
340-
341-
await executeWriteFileTool({ content: "&lt;test&gt;" })
342-
343-
expect(mockedUnescapeHtmlEntities).not.toHaveBeenCalled()
328+
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("&lt;test&gt;", true)
344329
})
345330

346331
it("strips line numbers from numbered content", async () => {

0 commit comments

Comments
 (0)