mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 04:55:44 +00:00
fix(ui): keep oversized chat replies readable (#45559)
* fix(ui): keep oversized chat replies readable * Update ui/src/ui/markdown.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix(ui): preserve oversized markdown whitespace --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1302,6 +1302,14 @@
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.markdown-plain-text-fallback {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
Lists
|
||||
=========================================== */
|
||||
|
||||
@@ -105,6 +105,47 @@ describe("toSanitizedMarkdownHtml", () => {
|
||||
expect(html).toContain("link");
|
||||
});
|
||||
|
||||
it("keeps oversized plain-text replies readable instead of forcing code-block chrome", () => {
|
||||
const input =
|
||||
Array.from(
|
||||
{ length: 320 },
|
||||
(_, i) => `Paragraph ${i + 1}: ${"Long plain-text reply. ".repeat(8)}`,
|
||||
).join("\n\n") + "\n";
|
||||
|
||||
const html = toSanitizedMarkdownHtml(input);
|
||||
|
||||
expect(html).not.toContain('<pre class="code-block">');
|
||||
expect(html).toContain('class="markdown-plain-text-fallback"');
|
||||
expect(html).toContain("Paragraph 1:");
|
||||
expect(html).toContain("Paragraph 320:");
|
||||
});
|
||||
|
||||
it("preserves indentation in oversized plain-text replies", () => {
|
||||
const input = `${"Header line\n".repeat(5000)}\n indented log line\n deeper indent`;
|
||||
const html = toSanitizedMarkdownHtml(input);
|
||||
|
||||
expect(html).toContain('class="markdown-plain-text-fallback"');
|
||||
expect(html).toContain(" indented log line");
|
||||
expect(html).toContain(" deeper indent");
|
||||
});
|
||||
|
||||
it("exercises the cached oversized fallback branch", () => {
|
||||
const input =
|
||||
Array.from(
|
||||
{ length: 240 },
|
||||
(_, i) => `Paragraph ${i + 1}: ${"Cacheable long reply. ".repeat(8)}`,
|
||||
).join("\n\n") + "\n";
|
||||
|
||||
expect(input.length).toBeGreaterThan(40_000);
|
||||
expect(input.length).toBeLessThan(50_000);
|
||||
|
||||
const first = toSanitizedMarkdownHtml(input);
|
||||
const second = toSanitizedMarkdownHtml(input);
|
||||
|
||||
expect(first).toContain('class="markdown-plain-text-fallback"');
|
||||
expect(second).toBe(first);
|
||||
});
|
||||
|
||||
it("falls back to escaped plain text if marked.parse throws (#36213)", () => {
|
||||
const parseSpy = vi.spyOn(marked, "parse").mockImplementation(() => {
|
||||
throw new Error("forced parse failure");
|
||||
|
||||
@@ -124,8 +124,10 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
|
||||
? `\n\n… truncated (${truncated.total} chars, showing first ${truncated.text.length}).`
|
||||
: "";
|
||||
if (truncated.text.length > MARKDOWN_PARSE_LIMIT) {
|
||||
const escaped = escapeHtml(`${truncated.text}${suffix}`);
|
||||
const html = `<pre class="code-block">${escaped}</pre>`;
|
||||
// Large plain-text replies should stay readable without inheriting the
|
||||
// capped code-block chrome, while still preserving whitespace for logs
|
||||
// and other structured text that commonly trips the parse guard.
|
||||
const html = renderEscapedPlainTextHtml(`${truncated.text}${suffix}`);
|
||||
const sanitized = DOMPurify.sanitize(html, sanitizeOptions);
|
||||
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
|
||||
setCachedMarkdown(input, sanitized);
|
||||
@@ -218,3 +220,7 @@ function escapeHtml(value: string): string {
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderEscapedPlainTextHtml(value: string): string {
|
||||
return `<div class="markdown-plain-text-fallback">${escapeHtml(value.replace(/\r\n?/g, "\n"))}</div>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user