Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 6 additions & 2 deletions src/core/tools/applyDiffTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import path from "path"
import fs from "fs/promises"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { telemetryService } from "../../services/telemetry/TelemetryService"

import { unescapeHtmlEntities } from "../../utils/text-normalization"
export async function applyDiffTool(
cline: Cline,
block: ToolUse,
Expand All @@ -21,7 +21,11 @@ export async function applyDiffTool(
removeClosingTag: RemoveClosingTag,
) {
const relPath: string | undefined = block.params.path
const diffContent: string | undefined = block.params.diff
let diffContent: string | undefined = block.params.diff

if (diffContent && !cline.api.getModel().id.includes("claude")) {
diffContent = unescapeHtmlEntities(diffContent)
}

const sharedMessageProps: ClineSayTool = {
tool: "appliedDiff",
Expand Down
5 changes: 3 additions & 2 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Cline } from "../Cline"
import { ToolUse } from "../assistant-message"
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
import { formatResponse } from "../prompts/responses"
import { unescapeHtmlEntities } from "../../utils/text-normalization"

export async function executeCommandTool(
cline: Cline,
Expand Down Expand Up @@ -32,8 +33,8 @@ export async function executeCommandTool(
return
}

// unescape html entities (e.g. &lt; -> <)
command = command.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&")
// Unescape HTML entities
command = unescapeHtmlEntities(command)

cline.consecutiveMistakeCount = 0

Expand Down
9 changes: 2 additions & 7 deletions src/core/tools/writeToFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { everyLineHasLineNumbers } from "../../integrations/misc/extract-text"
import delay from "delay"
import { detectCodeOmission } from "../../integrations/editor/detect-omission"
import { unescapeHtmlEntities } from "../../utils/text-normalization"

export async function writeToFileTool(
cline: Cline,
Expand Down Expand Up @@ -60,13 +61,7 @@ export async function writeToFileTool(
}

if (!cline.api.getModel().id.includes("claude")) {
// it seems not just llama models are doing cline, but also gemini and potentially others
if (newContent.includes("&gt;") || newContent.includes("&lt;") || newContent.includes("&quot;")) {
newContent = newContent
.replace(/&gt;/g, ">")
.replace(/&lt;/g, "<")
.replace(/&quot;/g, '"')
}
newContent = unescapeHtmlEntities(newContent)
}

// Determine if the path is outside the workspace
Expand Down
48 changes: 47 additions & 1 deletion src/utils/__tests__/text-normalization.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { normalizeString } from "../text-normalization"
import { normalizeString, unescapeHtmlEntities } from "../text-normalization"

describe("Text normalization utilities", () => {
describe("normalizeString", () => {
Expand Down Expand Up @@ -30,4 +30,50 @@ describe("Text normalization utilities", () => {
expect(normalizeString(input)).toBe('Let\'s test this-with some "fancy" punctuation... and spaces')
})
})

describe("unescapeHtmlEntities", () => {
test("unescapes basic HTML entities", () => {
expect(unescapeHtmlEntities("&lt;div&gt;Hello&lt;/div&gt;")).toBe("<div>Hello</div>")
})

test("unescapes ampersand entity", () => {
expect(unescapeHtmlEntities("This &amp; that")).toBe("This & that")
})

test("unescapes quote entities", () => {
expect(unescapeHtmlEntities("&quot;quoted&quot; and &#39;single-quoted&#39;")).toBe(
"\"quoted\" and 'single-quoted'",
)
})

test("unescapes apostrophe entity", () => {
expect(unescapeHtmlEntities("Don&apos;t worry")).toBe("Don't worry")
})

test("handles mixed content with multiple entity types", () => {
expect(
unescapeHtmlEntities(
"&lt;a href=&quot;https://example.com?param1=value&amp;param2=value&quot;&gt;Link&lt;/a&gt;",
),
).toBe('<a href="https://example.com?param1=value&param2=value">Link</a>')
})

test("handles mixed content with apostrophe entities", () => {
expect(
unescapeHtmlEntities(
"&lt;div&gt;Don&apos;t forget that Tom&amp;Jerry&apos;s show is at 3 o&apos;clock&lt;/div&gt;",
),
).toBe("<div>Don't forget that Tom&Jerry's show is at 3 o'clock</div>")
})

test("returns original string when no entities are present", () => {
const original = "Plain text without entities"
expect(unescapeHtmlEntities(original)).toBe(original)
})

test("handles empty or undefined input", () => {
expect(unescapeHtmlEntities("")).toBe("")
expect(unescapeHtmlEntities(undefined as unknown as string)).toBe(undefined)
})
})
})
18 changes: 18 additions & 0 deletions src/utils/text-normalization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,21 @@

return normalized
}

/**
* Unescapes common HTML entities in a string
*
* @param text The string containing HTML entities to unescape
* @returns The unescaped string with HTML entities converted to their literal characters
*/
export function unescapeHtmlEntities(text: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

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

The unescapeHtmlEntities function returns the input if falsy. Consider updating its type signature (e.g., accepting string | undefined) to avoid casting in tests and improve robustness against null/undefined values.

Suggested change
export function unescapeHtmlEntities(text: string): string {
export function unescapeHtmlEntities(text: string | undefined): string | undefined {

if (!text) return text

return text
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
}