From 7bca896cec3257693d00edbd849ca27d98866cb7 Mon Sep 17 00:00:00 2001 From: Agent Manager Date: Thu, 13 Aug 2026 16:45:28 +0000 Subject: [PATCH] Mobile: stop the conversation scrolling sideways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from a phone: the rendered conversation scrolls horizontally, and the scroll is not needed. Two separate causes, measured at a 390px viewport rather than read off the CSS — the page itself never scrolled, `.cxv-body` and the answer did. **The prompt band, 6px.** #49 gave the band a full bleed with `margin-right: -14px` matching `.cxv-body`'s 14px padding, but the phone breakpoint narrows that padding to 8px. The band stayed 14px wide into an 8px gutter, so the reading column was 6px wider than its own scroller. The gutter is now `--cx-gutter` and the bleed is `calc(-1 * var(--cx-gutter))`: one number per breakpoint, and the band cannot drift from it again. **Prose that could not wrap, 282px.** A URL and a long identifier are each one unbroken word, and one long word makes a paragraph wider than a phone pane. `.markdown` sets `overflow-y: auto`, which makes the other axis a scroller too, so the answer itself became draggable sideways. Prose now wraps (`overflow-wrap: anywhere` on p/li/blockquote/headings and on inline code, not on code inside `pre`). What deliberately still scrolls, inside its own box: `pre` (796px of content at this viewport) and `table` (217px). Wrapping a line of code to fit would change what it says, and `overflow-x: hidden` on a parent would clip the columns of a table people need to read. Verified with playwright at 320/390/768px, at 100% and 200% reader zoom: no horizontal scroll room on the page, on `.cxv-body`, on `.cxv-col` or on the answer, and `pre`/`table` still scroll. Desktop reader and the Overview card are pixel-identical before and after apart from the animated status dot in the footer (56 of 1.15M pixels, an 8×8 box). Server and web suites green. Co-Authored-By: Claude Opus 5 --- docs/conversation-view.md | 12 +++++++++++- web/src/conversation.css | 10 +++++++--- web/src/styles.css | 11 +++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/conversation-view.md b/docs/conversation-view.md index 398c5c5..a877415 100644 --- a/docs/conversation-view.md +++ b/docs/conversation-view.md @@ -256,7 +256,17 @@ pane with nothing to render (a shell) simply stays a terminal. - **The prompt band spans the pane.** Reaching into the left gutter but stopping at the text column on the right made it read as a card floating over the answer rather than as the head of it. Full bleed both sides; the meta row sits tight under the band it belongs to, and one - exchange ends well before the next begins. + exchange ends well before the next begins. The bleed is `var(--cx-gutter)`, the same variable + `.cxv-body` pays out as padding, because the phone breakpoint narrows that padding: a bleed + that restated the desktop number stayed 14px against 8px of gutter and made the conversation + column scroll sideways on a phone. +- **Nothing in the conversation scrolls sideways except the things meant to.** A code block and a + wide table each scroll inside their own box (`overflow-x: auto` on `pre` and `table`); the page, + the reading column and the answer do not. Prose therefore has to *wrap*, including a long URL + or an unbroken identifier — one long word is enough to push a paragraph wider than a phone, and + `.markdown` turns that into a sideways drag because a scroller in one axis makes the other axis + a scroller too. `overflow-x: hidden` on a parent is not the fix: it hides the symptom and clips + content people need to read. - **The reader fills the pane.** A fixed reading column left a gutter of nothing on each side while the prompt band still spanned the full width, so the two disagreed about where the conversation began. The pane is the measure: narrow the pane and the conversation narrows. diff --git a/web/src/conversation.css b/web/src/conversation.css index 3d21b51..3e67d0a 100644 --- a/web/src/conversation.css +++ b/web/src/conversation.css @@ -192,7 +192,11 @@ mark.cx-hit.on { background: var(--accent); color: var(--panel); } you scan for — still spanned the full width, so the two disagreed about where the conversation began. The pane is the measure: make it narrow and the conversation is narrow. */ -.cxv-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; background: var(--panel); padding: 4px 14px 30px; } +/* The gutter is a variable because the prompt band bleeds INTO it (below). A + hardcoded bleed against a padding that changes at a breakpoint drifts apart, + and the band ends up wider than the scroller it sits in — which is exactly + how the phone gained a sideways scroll: 14px of bleed, 8px of padding. */ +.cxv-body { --cx-gutter: 14px; flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; background: var(--panel); padding: 4px var(--cx-gutter) 30px; } .cxv-col { display: flex; flex-direction: column; min-width: 0; } .cxv-msg { font-size: 0.85em; color: var(--muted); padding: 6px 0; } /* The line above the oldest loaded exchange. Fixed height on purpose: it sits @@ -231,7 +235,7 @@ mark.cx-hit.on { background: var(--accent); color: var(--panel); } text column on the right made it read as a card floating over the answer rather than as the head of it. Full bleed both sides, with the text inside still on the column. */ -.cxv-col .cx-prompt { margin-right: -14px; padding-right: 14px; } +.cxv-col .cx-prompt { margin-right: calc(-1 * var(--cx-gutter)); padding-right: var(--cx-gutter); } .cxv-body > .cxv-col > .cx { padding-left: 16px; } /* Grouping: the meta row is the prompt's, not the answer's, so it sits tight @@ -241,7 +245,7 @@ mark.cx-hit.on { background: var(--accent); color: var(--panel); } /* ---------- phone ---------- */ @media (max-width: 720px) { - .cxv-body { padding: 4px 8px 24px; } + .cxv-body { --cx-gutter: 8px; padding: 4px var(--cx-gutter) 24px; } .cs-detail { font-size: 1em; } } diff --git a/web/src/styles.css b/web/src/styles.css index d6d83b6..ee57984 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1147,6 +1147,17 @@ a.btn-ghost { text-decoration: none; } .markdown th, .markdown td { border: 1px solid var(--border); padding: 6px 10px; } .markdown img { max-width: 100%; } .markdown hr { border: none; border-top: 1px solid var(--border); margin: 16px 0; } +/* Prose wraps; only genuinely wide content scrolls. A URL or an unbroken + identifier is one long word, and a long word made the paragraph wider than + the pane — which .markdown then scrolled sideways, because a scroller in one + axis makes the other axis a scroller too (overflow-y: auto above). Reported + on a phone, where the pane is narrow enough for ordinary prose to do it. + `pre` and `table` keep their own overflow-x: auto: a code block or a wide + table is meant to scroll, inside its own box, and breaking a line of code to + fit would change what it says. */ +.markdown p, .markdown li, .markdown blockquote, +.markdown h1, .markdown h2, .markdown h3, .markdown h4 { overflow-wrap: anywhere; } +.markdown :not(pre) > code { overflow-wrap: anywhere; } /* usage page */ .usage { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; }