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

Commit 1ef31a7

Browse files
committed
fix: remove HTML entity unescaping from all tool implementations - Fixes #9563
1 parent 78dc344 commit 1ef31a7

6 files changed

Lines changed: 7 additions & 85 deletions

File tree

src/core/tools/ApplyDiffTool.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { Task } from "../task/Task"
1010
import { formatResponse } from "../prompts/responses"
1111
import { fileExistsAtPath } from "../../utils/fs"
1212
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
13-
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1413
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1514
import { computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
1615
import { BaseTool, ToolCallbacks } from "./BaseTool"
@@ -35,10 +34,6 @@ export class ApplyDiffTool extends BaseTool<"apply_diff"> {
3534
const { askApproval, handleError, pushToolResult, toolProtocol } = callbacks
3635
let { path: relPath, diff: diffContent } = params
3736

38-
if (diffContent && !task.api.getModel().id.includes("claude")) {
39-
diffContent = unescapeHtmlEntities(diffContent)
40-
}
41-
4237
try {
4338
if (!relPath) {
4439
task.consecutiveMistakeCount++

src/core/tools/ExecuteCommandTool.ts

Lines changed: 3 additions & 7 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"
@@ -58,8 +57,7 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
5857

5958
task.consecutiveMistakeCount = 0
6059

61-
const unescapedCommand = unescapeHtmlEntities(command)
62-
const didApprove = await askApproval("command", unescapedCommand)
60+
const didApprove = await askApproval("command", command)
6361

6462
if (!didApprove) {
6563
return
@@ -86,16 +84,14 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
8684
.get<string[]>("commandTimeoutAllowlist", [])
8785

8886
// Check if command matches any prefix in the allowlist
89-
const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) =>
90-
unescapedCommand.startsWith(prefix.trim()),
91-
)
87+
const isCommandAllowlisted = commandTimeoutAllowlist.some((prefix) => command.startsWith(prefix.trim()))
9288

9389
// Convert seconds to milliseconds for internal use, but skip timeout if command is allowlisted
9490
const commandExecutionTimeout = isCommandAllowlisted ? 0 : commandExecutionTimeoutSeconds * 1000
9591

9692
const options: ExecuteCommandOptions = {
9793
executionId,
98-
command: unescapedCommand,
94+
command,
9995
customCwd,
10096
terminalShellIntegrationDisabled,
10197
terminalOutputLineLimit,

src/core/tools/MultiApplyDiffTool.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { ToolUse, RemoveClosingTag, AskApproval, HandleError, PushToolResult } f
1111
import { formatResponse } from "../prompts/responses"
1212
import { fileExistsAtPath } from "../../utils/fs"
1313
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
14-
import { unescapeHtmlEntities } from "../../utils/text-normalization"
1514
import { parseXmlForDiff } from "../../utils/xml"
1615
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1716
import { applyDiffTool as applyDiffToolClass } from "./ApplyDiffTool"
@@ -204,7 +203,7 @@ Original error: ${errorMessage}`
204203
path: legacyPath,
205204
diff: [
206205
{
207-
content: legacyDiffContent, // Unescaping will be handled later like new diffs
206+
content: legacyDiffContent,
208207
startLine: legacyStartLineStr ? parseInt(legacyStartLineStr) : undefined,
209208
},
210209
],
@@ -322,12 +321,7 @@ Original error: ${errorMessage}`
322321
let unified = ""
323322
try {
324323
const original = await fs.readFile(opResult.absolutePath!, "utf-8")
325-
const processed = !cline.api.getModel().id.includes("claude")
326-
? (opResult.diffItems || []).map((item) => ({
327-
...item,
328-
content: item.content ? unescapeHtmlEntities(item.content) : item.content,
329-
}))
330-
: opResult.diffItems || []
324+
const processed = opResult.diffItems || []
331325

332326
const applyRes =
333327
(await cline.diffStrategy?.applyDiff(original, processed)) ?? ({ success: false } as any)
@@ -484,12 +478,8 @@ Original error: ${errorMessage}`
484478
let formattedError = ""
485479

486480
// Pre-process all diff items for HTML entity unescaping if needed
487-
const processedDiffItems = !cline.api.getModel().id.includes("claude")
488-
? diffItems.map((item) => ({
489-
...item,
490-
content: item.content ? unescapeHtmlEntities(item.content) : item.content,
491-
}))
492-
: diffItems
481+
// Use diff items directly without HTML entity unescaping
482+
const processedDiffItems = diffItems
493483

494484
// Apply all diffs at once with the array-based method
495485
const diffResult = (await cline.diffStrategy?.applyDiff(originalContent, processedDiffItems)) ?? {

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 { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
1615
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1716
import { convertNewFileToUnifiedDiff, computeDiffStats, sanitizeUnifiedDiff } from "../diff/stats"
@@ -88,10 +87,6 @@ export class WriteToFileTool extends BaseTool<"write_to_file"> {
8887
newContent = newContent.split("\n").slice(0, -1).join("\n")
8988
}
9089

91-
if (!task.api.getModel().id.includes("claude")) {
92-
newContent = unescapeHtmlEntities(newContent)
93-
}
94-
9590
const fullPath = relPath ? path.resolve(task.cwd, removeClosingTag("path", relPath)) : ""
9691
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
9792

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

Lines changed: 0 additions & 31 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, RemoveClosingTag } from "../../../shared/tools"
9-
import { unescapeHtmlEntities } from "../../../utils/text-normalization"
109

1110
// Mock dependencies
1211
vitest.mock("execa", () => ({
@@ -105,36 +104,6 @@ describe("executeCommandTool", () => {
105104
}
106105
})
107106

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

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

Lines changed: 0 additions & 23 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

@@ -322,22 +315,6 @@ describe("writeToFileTool", () => {
322315
expect(mockCline.diffViewProvider.update).toHaveBeenCalledWith("", true)
323316
})
324317

325-
it("unescapes HTML entities for non-Claude models", async () => {
326-
mockCline.api.getModel.mockReturnValue({ id: "gpt-4" })
327-
328-
await executeWriteFileTool({ content: "&lt;test&gt;" })
329-
330-
expect(mockedUnescapeHtmlEntities).toHaveBeenCalledWith("&lt;test&gt;")
331-
})
332-
333-
it("skips HTML unescaping for Claude models", async () => {
334-
mockCline.api.getModel.mockReturnValue({ id: "claude-3" })
335-
336-
await executeWriteFileTool({ content: "&lt;test&gt;" })
337-
338-
expect(mockedUnescapeHtmlEntities).not.toHaveBeenCalled()
339-
})
340-
341318
it("strips line numbers from numbered content", async () => {
342319
const contentWithLineNumbers = "1 | line one\n2 | line two"
343320
mockedEveryLineHasLineNumbers.mockReturnValue(true)

0 commit comments

Comments
 (0)