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();
     }