From 064118d329a02aa0bebb30e482f490af778f0dbf Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 23 Jun 2026 03:54:20 +0000 Subject: [PATCH] fix(ui): wrap message footer on narrow screens instead of overflowing (#4724-followup) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The assistant message footer (model · duration · tokens · cost · cache% · time · actions) could overflow its row on mobile/narrow panes, pushing the per-message action buttons off the right edge where they were unreachable. #4724 proposed ellipsing every stat, which shrank them into unreadable stubs (D…, 98…). This instead lets .msg-foot flex-wrap to a second line: every stat stays full-size + readable, the actions/time are pinned flex:0 0 auto so they never shrink and stay on-screen, and on wider screens where it already fits wrap never triggers. Pure CSS. + source-structure regression test. Co-authored-by: starship-s --- CHANGELOG.md | 6 ++++ static/style.css | 12 +++++-- tests/test_msg_footer_wrap.py | 66 +++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 tests/test_msg_footer_wrap.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f930eb3..1a9941c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.597] — 2026-06-23 — Release VD (message footer wraps instead of overflowing on narrow screens) + +### Fixed + +- **On a phone or a narrow pane, a message's action buttons (edit / copy / retry) no longer get pushed off the right edge.** When an assistant message footer carried a lot of metadata — a long model name plus duration, token counts, cost, and cache-hit percent — the row could exceed the available width and shove the per-message action buttons off-screen, where they were unreachable on mobile. The footer now wraps to a second line when it doesn't fit, so every stat stays fully readable and the action buttons stay on-screen. On wider screens where it already fits, nothing changes. Thanks @starship-s for surfacing the overflow. (#4724-followup) + ## [v0.51.596] — 2026-06-23 — Release VC (throttle reasoning SSE to stop tab freeze) ### Fixed diff --git a/static/style.css b/static/style.css index 4cbb8019c..dd9c8cbbf 100644 --- a/static/style.css +++ b/static/style.css @@ -5119,13 +5119,21 @@ main.main > #mainPlugin{display:none;} display: flex; align-items: center; justify-content: flex-end; + /* #4724-followup: wrap to a second line on narrow screens (mobile / split panes) + instead of overflowing the row. A long model name + duration + token/cost stats + can exceed the width; without wrap the per-message action buttons get pushed off + the right edge and become unreachable on mobile. Wrapping keeps every stat fully + readable (no ellipsis) and the buttons on-screen; on desktop where it already fits, + wrap never triggers so nothing changes. */ + flex-wrap: wrap; + row-gap: 2px; gap: 6px; margin-top: 4px; font-size: 11px; color: var(--muted); } -.msg-foot .msg-actions { opacity: 1; margin-left: 0; } -.msg-foot .msg-time { font-size: 10.5px; opacity: .75; } +.msg-foot .msg-actions { opacity: 1; margin-left: 0; flex: 0 0 auto; } +.msg-foot .msg-time { font-size: 10.5px; opacity: .75; flex: 0 0 auto; } .session-jump-btn--inline { position: static; right: auto; diff --git a/tests/test_msg_footer_wrap.py b/tests/test_msg_footer_wrap.py new file mode 100644 index 000000000..e8c91c6a9 --- /dev/null +++ b/tests/test_msg_footer_wrap.py @@ -0,0 +1,66 @@ +"""Regression test for the #4724-followup message-footer wrap fix. + +The bug: on a narrow screen (mobile / split pane) the assistant message footer +(model name · duration · token/cost stats · timestamp · action buttons) overflowed +its row, pushing the per-message action buttons (edit/copy/retry) off the right edge +where they were unreachable. + +The rejected approach (#4724) made every usage span `flex: 0 1 auto` + ellipsis, which +shrank the stats into unreadable stubs (`D…`, `98…`). This fix instead lets `.msg-foot` +WRAP to a second line — every stat stays fully readable and the buttons stay on-screen. + +These are source-structure assertions on static/style.css (CSS isn't executed in the +suite), pinning the wrap behavior so it can't silently regress. +""" +import pathlib + +REPO = pathlib.Path(__file__).parent.parent +CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8") + + +def _rule_body(selector_literal: str) -> str: + """Return the declaration block for the first rule whose head exactly matches.""" + idx = CSS.find(selector_literal + " {") + if idx == -1: + idx = CSS.find(selector_literal + "{") + assert idx != -1, f"rule {selector_literal!r} not found in style.css" + brace = CSS.index("{", idx) + end = CSS.index("}", brace) + return CSS[brace + 1:end] + + +def test_msg_foot_wraps_not_overflows(): + body = _rule_body(".msg-foot") + assert "flex-wrap: wrap" in body, ( + ".msg-foot must flex-wrap so a long footer wraps to a second line on narrow " + "screens instead of overflowing and pushing the action buttons off-screen" + ) + # must still be a flex row + assert "display: flex" in body + + +def test_msg_foot_controls_pinned_not_shrunk(): + # The actions + timestamp must NOT shrink/ellipsis — they stay full size and wrap + # as whole units, so the buttons are always intact and reachable. + actions = _rule_body(".msg-foot .msg-actions") + time = _rule_body(".msg-foot .msg-time") + assert "flex: 0 0 auto" in actions, ".msg-actions must be flex:0 0 auto (never shrink)" + assert "flex: 0 0 auto" in time, ".msg-time must be flex:0 0 auto (never shrink)" + + +def test_usage_stats_not_ellipsis_shrunk(): + # Guard against regressing to the rejected ellipsis-everything approach: the usage + # spans must NOT be flex:0 1 auto with text-overflow:ellipsis (that produced the + # unreadable `D…` / `98…` stubs). They keep flex:0 0 auto and wrap as whole units. + # The usage spans share a grouped rule (.msg-usage-inline, .msg-duration-inline, …), + # so extract that group's block by its leading selector then the next `{`. + head = CSS.index(".msg-usage-inline,") + brace = CSS.index("{", head) + body = CSS[brace + 1: CSS.index("}", brace)] + assert "flex: 0 0 auto" in body, ( + "usage spans must stay flex:0 0 auto (full size, wrap as a unit) — not " + "flex:0 1 auto+ellipsis, which shrinks them into unreadable stubs" + ) + assert "text-overflow: ellipsis" not in body, ( + "usage spans must not ellipsis-truncate; the footer wraps instead" + )