Skip to content

Commit d5f862a

Browse files
forestyooJimmycaidaniel-lxs
authored
fix: TaskItem display and copy issues with HTML tags in task messages. (#4444)
* fix: TaskItem display and copy issues with HTML tags in task messages. * Display issue: HTML tags not displaying in HistoryView. * Copy issue: HTML tags in message are removed when clicking the copy button in TaskItem. * perf(webview-ui): add LRU cache to escapeHtml function - Add LRU cache with 500 item limit for escapeHtml results - Improves performance by caching frequently escaped strings - Reduces redundant HTML escaping operations --------- Co-authored-by: Jimmycai <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent d7087c3 commit d5f862a

File tree

4 files changed

+40
-21
lines changed

4 files changed

+40
-21
lines changed

webview-ui/src/components/history/CopyButton.tsx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,16 @@ type CopyButtonProps = {
1010
className?: string
1111
}
1212

13-
/**
14-
* Strips only history highlight spans from text while preserving other HTML
15-
* Targets: <span class="history-item-highlight">content</span>
16-
* @param text - Text that may contain highlight spans
17-
* @returns Text with highlight spans removed but content preserved
18-
*/
19-
const stripHistoryHighlightSpans = (text: string): string => {
20-
// Match opening tag, capture content until closing tag
21-
// The [\s\S]*? pattern matches any character (including newlines) non-greedily,
22-
// which properly handles content with < characters
23-
return text.replace(/<span\s+class="history-item-highlight">([\s\S]*?)<\/span>/g, "$1")
24-
}
25-
2613
export const CopyButton = ({ itemTask, className }: CopyButtonProps) => {
2714
const { isCopied, copy } = useClipboard()
2815
const { t } = useAppTranslation()
2916

3017
const onCopy = useCallback(
3118
(e: React.MouseEvent) => {
3219
e.stopPropagation()
20+
3321
if (!isCopied) {
34-
// Strip only history highlight spans before copying to clipboard
35-
const cleanText = stripHistoryHighlightSpans(itemTask)
36-
copy(cleanText)
22+
copy(itemTask)
3723
}
3824
},
3925
[isCopied, copy, itemTask],

webview-ui/src/components/history/TaskItem.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
99
import TaskItemHeader from "./TaskItemHeader"
1010
import TaskItemFooter from "./TaskItemFooter"
1111

12+
interface DisplayHistoryItem extends HistoryItem {
13+
highlight?: string
14+
}
15+
1216
interface TaskItemProps {
13-
item: HistoryItem
17+
item: DisplayHistoryItem
1418
variant: "compact" | "full"
1519
showWorkspace?: boolean
1620
isSelectionMode?: boolean
@@ -103,8 +107,8 @@ const TaskItem = ({
103107
overflowWrap: "anywhere",
104108
}}
105109
data-testid={isCompact ? undefined : "task-content"}
106-
{...(isCompact ? {} : { dangerouslySetInnerHTML: { __html: item.task } })}>
107-
{isCompact ? item.task : undefined}
110+
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
111+
{item.highlight ? undefined : item.task}
108112
</div>
109113

110114
{/* Task Item Footer */}

webview-ui/src/components/history/useTaskSearch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const useTaskSearch = () => {
4848

4949
return {
5050
...result.item,
51-
task: highlightFzfMatch(
51+
highlight: highlightFzfMatch(
5252
result.item.task,
5353
positions.filter((p) => p < taskEndIndex),
5454
),

webview-ui/src/utils/highlight.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
import { LRUCache } from "lru-cache"
2+
3+
// LRU cache for escapeHtml with reasonable size limit
4+
const escapeHtmlCache = new LRUCache<string, string>({ max: 500 })
5+
6+
function escapeHtml(text: string): string {
7+
// Check cache first
8+
const cached = escapeHtmlCache.get(text)
9+
if (cached !== undefined) {
10+
return cached
11+
}
12+
13+
// Compute escaped text
14+
const escaped = text
15+
.replace(/&/g, "&amp;")
16+
.replace(/</g, "&lt;")
17+
.replace(/>/g, "&gt;")
18+
.replace(/"/g, "&quot;")
19+
.replace(/'/g, "&#39;")
20+
21+
// Cache the result
22+
escapeHtmlCache.set(text, escaped)
23+
24+
return escaped
25+
}
26+
127
export function highlightFzfMatch(
228
text: string,
329
positions: number[],
@@ -39,6 +65,9 @@ export function highlightFzfMatch(
3965

4066
// Build final string
4167
return parts
42-
.map((part) => (part.highlight ? `<span class="${highlightClassName}">${part.text}</span>` : part.text))
68+
.map((part) => {
69+
const escapedText = escapeHtml(part.text)
70+
return part.highlight ? `<span class="${highlightClassName}">${escapedText}</span>` : escapedText
71+
})
4372
.join("")
4473
}

0 commit comments

Comments
 (0)