mirror of https://github.com/openclaw/openclaw.git
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:
parent
89e52d6178
commit
40ab39b5ea
|
|
@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone.
|
||||
- Config/discovery: accept `discovery.wideArea.domain` in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.
|
||||
- Telegram/media errors: redact Telegram file URLs before building media fetch errors so failed inbound downloads do not leak bot tokens into logs. Thanks @space08.
|
||||
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
|
||||
|
||||
## 2026.3.12
|
||||
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue