From 16c4c995a847688bf12dad6661c8f6eae46aba3b Mon Sep 17 00:00:00 2001 From: sixvolts Date: Fri, 2 Oct 2026 20:39:03 +0000 Subject: [PATCH] fix(ui): wrap code blocks instead of scrolling them, in every view A long curl in a chat code block forced horizontal scrolling. Code blocks were set to scroll inside their own box, a deliberate choice in the chat-overflow fix (#16) that the operator has now reversed: code wraps. Measured, not inferred. chat-overflow.spec.ts was tightened first so it reproduces the bug, then the CSS was fixed against it: before: CODE.hljs scrollWidth 2834 in a 378px box -> test FAILS after: no element wider than the 408px bubble -> test PASSES The offender was the inner , not the
. highlight.js styles .hljs,
and text inside a non-wrapping  ignores its parent's white-space, so the
rule has to name the code element too. overflow-wrap:anywhere breaks unbroken
tokens (URLs, base64, long flags) that pre-wrap alone would leave hanging.

Same bug, fixed in every renderer of markdown code:
  desktop chat         .chat-msg-body pre (two conflicting rules, both scrolled)
  mobile chat          .mob-msg .bubble pre
  desktop notes/wiki   .notes-editor .toastui-editor-contents pre
  mobile notes         .mob-note-body .toastui-editor-contents pre
  action run detail    .action-run-detail pre (wrapped, but URLs escaped)

One block appended to each stylesheet rather than five edits in place: equal or
higher specificity and placed last, so it wins over the per-view rules and over
any highlight.js theme's lower-specificity .hljs rule. Corrected the earlier
comment that still said code blocks "cannot wrap".

The spec no longer exempts PRE/CODE from the no-horizontal-overflow check (only
TABLE may scroll inside itself), and asserts the 400-char line actually spans
several rendered lines unclipped. That second check exists because an earlier
overflow-x:hidden "passed" a no-scroll check by cutting the text off.

Only desktop chat is measured directly; the other four share the identical rule.
CI's mobile pixel baselines are the broader check on them.
---
 familiar-workspace/static/app.css     | 31 ++++++++++++++++++++++++---
 familiar-workspace/static/mobile.css  | 13 +++++++++++
 tests/e2e/flows/chat-overflow.spec.ts | 18 ++++++++++++++--
 3 files changed, 57 insertions(+), 5 deletions(-)

diff --git a/familiar-workspace/static/app.css b/familiar-workspace/static/app.css
index 767ac00..fd10200 100644
--- a/familiar-workspace/static/app.css
+++ b/familiar-workspace/static/app.css
@@ -3993,9 +3993,9 @@ code {
     /* Long URLs and unbroken tokens wrap instead of widening the panel. */
     overflow-wrap: anywhere;
 }
-/* Code blocks keep their formatting, so they cannot wrap — let them scroll
-   inside their own box rather than stretching the message column and taking
-   the whole conversation sideways with it. */
+/* Code blocks wrap rather than scroll: see the "Code blocks WRAP" block
+   at the end of this file. The width rules below still keep a long line
+   from sizing the message column. */
 .chat-msg-body pre {
     /* width:0 makes this contribute nothing to the parent's max-content size,
        so .chat-msg-body is sized by its other children instead of by the
@@ -6700,3 +6700,28 @@ code {
     white-space: nowrap;
     border: 0;
 }
+
+/* ── Code blocks WRAP, they don't scroll (2026-10-02) ────────────────────
+   Operator decision after a long curl in a chat code block forced horizontal
+   scrolling. This supersedes the earlier "let code blocks scroll inside their
+   own box" choice in every view that renders markdown code.
+
+   Wrapping has to reach the inner , not just the 
: highlight.js
+   styles .hljs, and text inside a non-wrapping  ignores its parent.
+   Measured before the fix: CODE.hljs scrollWidth 2834 in a 378px box.
+   overflow-wrap:anywhere breaks unbroken tokens (URLs, base64, long flags)
+   that pre-wrap alone would leave running off the edge.
+
+   Placed last so it wins over the per-view rules above at equal specificity,
+   and over any highlight.js theme's lower-specificity .hljs rule. Guarded by
+   tests/e2e/flows/chat-overflow.spec.ts, which now fails if a code block
+   scrolls or clips instead of wrapping. */
+.chat-msg-body pre,
+.chat-msg-body pre code,
+.chat-msg-body pre code.hljs,
+.notes-editor .toastui-editor-contents pre,
+.notes-editor .toastui-editor-contents pre code,
+.action-run-detail pre {
+    white-space: pre-wrap;
+    overflow-wrap: anywhere;
+}
diff --git a/familiar-workspace/static/mobile.css b/familiar-workspace/static/mobile.css
index 22b405f..82e6164 100644
--- a/familiar-workspace/static/mobile.css
+++ b/familiar-workspace/static/mobile.css
@@ -2318,3 +2318,16 @@ select.mob-sa-input {
     position: absolute;
     inset: -7px;
 }
+
+/* ── Code blocks WRAP, they don't scroll (2026-10-02) ────────────────────
+   Mirrors the same rule in app.css; see its comment. Mobile chat bubbles had
+   the identical bug (overflow-x:auto, no wrap) and a phone is where sideways
+   scrolling a code block is worst. */
+.mob-msg .bubble pre,
+.mob-msg .bubble pre code,
+.mob-msg .bubble pre code.hljs,
+.mob-note-body .toastui-editor-contents pre,
+.mob-note-body .toastui-editor-contents pre code {
+    white-space: pre-wrap;
+    overflow-wrap: anywhere;
+}
diff --git a/tests/e2e/flows/chat-overflow.spec.ts b/tests/e2e/flows/chat-overflow.spec.ts
index 31a9bd5..7023b58 100644
--- a/tests/e2e/flows/chat-overflow.spec.ts
+++ b/tests/e2e/flows/chat-overflow.spec.ts
@@ -116,11 +116,25 @@ test("the chat panel never scrolls horizontally", async ({ stack, browser, reque
             return out;
         });
 
-        // TABLE / PRE / the thinking trace scrolling inside themselves is by design.
+        // Only a TABLE (and the collapsible thinking trace) may scroll inside
+        // itself. Code blocks WRAP: operator decision 2026-10-02, after a long
+        // curl in a code block forced horizontal scrolling. PRE/CODE are no
+        // longer exempt, so a code block that scrolls fails here.
         const bad = offenders.filter(
-            (o) => !/^(TABLE|PRE|CODE)\./.test(o) && !/chat-msg-thinking-body/.test(o),
+            (o) => !/^TABLE\./.test(o) && !/chat-msg-thinking-body/.test(o),
         );
         expect(bad, "unexpected horizontal overflow:\n" + offenders.join("\n")).toEqual([]);
+
+        const pre = await page.evaluate(() => {
+            const el = document.querySelector(".chat-msg-body pre");
+            if (!el) return null;
+            const lh = parseFloat(getComputedStyle(el).lineHeight) || 18;
+            return { lines: el.clientHeight / lh, clipped: el.scrollWidth > el.clientWidth + 1,
+                     ws: getComputedStyle(el.querySelector("code") || el).whiteSpace };
+        });
+        expect(pre, "no code block rendered").not.toBeNull();
+        expect(pre!.clipped, "code block clips or scrolls instead of wrapping").toBe(false);
+        expect(pre!.lines, "400-char line should wrap onto several lines, ws=" + pre!.ws).toBeGreaterThan(3);
     } finally {
         await ctx.close();
     }