From bb687fa2de2ccbff1668bbff90813f5d1ab09017 Mon Sep 17 00:00:00 2001 From: Thiago Pontes Date: Mon, 27 Jul 2026 04:54:03 -0300 Subject: [PATCH] fix(ui): keep long Markdown previews scrollable (#113465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [AI] fix(ui): add min-height:0 to flex scroll containers for Firefox Firefox enforces the CSS Flexbox automatic minimum size more strictly than Chromium, preventing .sidebar-content and .md-preview-dialog__body from shrinking below their content height. This blocked overflow:auto from creating a scrollable area when Markdown content exceeded the available panel height. Add min-height:0 to both flex children so Firefox allows them to shrink and become the inner scroll container. Related to #107571 Co-Authored-By: Claude Sonnet 4.6 * [AI] fix(ui): bound Markdown sidebar previews * [AI] fix(ui): bound file sidebar editors * test(ui): cover scroll repair in sidebar column layout --------- Co-authored-by: 杨爱文 Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Thiago Pontes AI --- .../chat-sidebar-scroll.browser.test.ts | 82 +++++++++++++++++++ ui/src/pages/chat/components/chat-sidebar.ts | 6 +- ui/src/styles/chat/sidebar.css | 10 +++ ui/src/styles/components.css | 1 + 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 ui/src/pages/chat/components/chat-sidebar-scroll.browser.test.ts diff --git a/ui/src/pages/chat/components/chat-sidebar-scroll.browser.test.ts b/ui/src/pages/chat/components/chat-sidebar-scroll.browser.test.ts new file mode 100644 index 000000000000..6a79717a3f33 --- /dev/null +++ b/ui/src/pages/chat/components/chat-sidebar-scroll.browser.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from "vitest"; +import "../../../styles.css"; +import "../../../styles/chat.css"; +import type { SidebarContent } from "./chat-sidebar.ts"; +import "./chat-sidebar.ts"; + +const browserMode = "__vitest_browser__" in globalThis; + +type DetailPanel = HTMLElement & { + content: SidebarContent; + updateComplete: Promise; +}; + +describe.runIf(browserMode)("chat sidebar layout", () => { + it("keeps long markdown scrollable inside a bounded sidebar", async () => { + const container = document.createElement("div"); + container.className = "sidebar-column__panel"; + container.style.cssText = "display:flex;width:480px;height:320px;"; + + const panel = document.createElement("openclaw-chat-detail-panel") as DetailPanel; + panel.className = "chat-sidebar"; + panel.content = { + kind: "markdown", + content: Array.from( + { length: 40 }, + (_, index) => `## Section ${index + 1}\n\nLong preview content for scrolling.`, + ).join("\n\n"), + }; + container.append(panel); + document.body.append(container); + + try { + await panel.updateComplete; + const content = panel.querySelector(".sidebar-content"); + expect(content).not.toBeNull(); + expect(content!.clientHeight).toBeLessThan(content!.scrollHeight); + + content!.scrollTop = content!.scrollHeight; + await new Promise(requestAnimationFrame); + expect(content!.scrollTop).toBeGreaterThan(0); + } finally { + container.remove(); + } + }); + + it("keeps long files scrollable inside CodeMirror", async () => { + const container = document.createElement("div"); + container.className = "sidebar-column__panel"; + container.style.cssText = "display:flex;width:480px;height:320px;"; + + const panel = document.createElement("openclaw-chat-detail-panel") as DetailPanel; + panel.className = "chat-sidebar"; + panel.content = { + kind: "file", + path: "src/long-example.ts", + name: "long-example.ts", + language: "typescript", + content: Array.from( + { length: 200 }, + (_, index) => `export const value${index + 1} = ${index + 1};`, + ).join("\n"), + }; + container.append(panel); + document.body.append(container); + + try { + await panel.updateComplete; + await expect + .poll(() => panel.querySelector(".cm-scroller"), { timeout: 5_000 }) + .not.toBeNull(); + const scroller = panel.querySelector(".cm-scroller"); + expect(scroller).not.toBeNull(); + expect(scroller!.clientHeight).toBeLessThan(scroller!.scrollHeight); + + scroller!.scrollTop = scroller!.scrollHeight; + await new Promise(requestAnimationFrame); + expect(scroller!.scrollTop).toBeGreaterThan(0); + } finally { + container.remove(); + } + }); +}); diff --git a/ui/src/pages/chat/components/chat-sidebar.ts b/ui/src/pages/chat/components/chat-sidebar.ts index 1cb0d25298a9..a0d0ff84150a 100644 --- a/ui/src/pages/chat/components/chat-sidebar.ts +++ b/ui/src/pages/chat/components/chat-sidebar.ts @@ -1280,8 +1280,12 @@ class ChatDetailPanel extends OpenClawLightDomElement { const currentMatchIndex = matches.length ? Math.min(this.fileSearchMatchIndex, matches.length - 1) : 0; + // Markdown previews and file editors need a bounded host wrapper so their + // inner content can shrink and scroll. Content-sized kinds keep auto height. + const fillHost = + this.visibleContent?.kind === "file" || this.visibleContent?.kind === "markdown"; return html` -
+
${renderMarkdownSidebar({ content: this.visibleContent, error: this.error, diff --git a/ui/src/styles/chat/sidebar.css b/ui/src/styles/chat/sidebar.css index 93a92275c8d5..4a3ad281901b 100644 --- a/ui/src/styles/chat/sidebar.css +++ b/ui/src/styles/chat/sidebar.css @@ -1347,10 +1347,20 @@ openclaw-chat-sidebar-region, .sidebar-content { flex: 1; + min-height: 0; overflow: auto; padding: 16px; } +/* Full-height panel kinds need a bounded wrapper so their inner content can + shrink and scroll instead of expanding past the rail. */ +.sidebar-panel-host--fill { + display: flex; + flex: 1; + flex-direction: column; + min-height: 0; +} + openclaw-session-discussion { display: flex; flex: 1 1 0; diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index d7d2b6caa40f..ca2234d2e6ab 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -4040,6 +4040,7 @@ td.data-table-key-col { .md-preview-dialog__body { flex: 1; + min-height: 0; overflow: auto; overscroll-behavior: contain; padding: clamp(18px, 3vw, 28px);