From c4357fc9e3367011f2d95a69e3fd3065e039070c Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Sat, 15 Aug 2026 07:19:34 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=90=20feat:=20Match=20the=20Message=20?= =?UTF-8?q?Column=20to=20the=20Composer=20(#14851)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Match the message column to the composer width Give messages the same max-width and horizontal padding as ChatForm, reserve the scrollbar gutter on the composer wrapper, and drop the 65ch prose cap so the body fills that column. * style: Drop the assistant avatar gutter Keep the icon and provider name on the same left edge as the message body. Mid-message author headers and steer bubbles no longer outdent past a column that no longer exists. * style: Reveal the timestamp on the message header bar Put the icon, provider name, and datetime on one full-width row, and show the time only when the message is hovered or focused. CSS on .message-render wins the hover hide that Tailwind group-hover lost. * style: Align scroll-to-bottom with the chat column Sit the control in the same padded column as the composer, swap the hard-coded disc for a themed outline Button, and fade it in on an 8px rise instead of a scale pop. * feat: Crossfade the provider name to the model on hover Swap the assistant header label to the real model name when the provider is hovered or focused. Skip agent_* document ids so the hover text is only a model name. * fix: Reserve the message column gutter without clipping the composer `scrollbar-gutter: stable` only holds its band back while the element is a scroll container, and a scroll container clips. Wrapping the composer in one put the in-flight steer overlay outside the clip: it is painted above the composer's top edge, so for the whole run a submitted steer was invisible and its cancel unreachable. The scroll-to-bottom wrapper had the same problem in a smaller way, cutting off the button's focus ring. Reserve the same band with padding instead, sized by the width the app already gives its own scrollbars, so both columns still line up with the messages without either becoming a scroll container. Also scopes the header label crossfade to the two labelled spans, and drops its `:focus-within` rules, which no focusable descendant can ever trigger. * fix: Keep document ids out of the header and name the model to screen readers An Assistants-endpoint message keys the assistant map by `assistant.id`, so its `model` field holds an `asst_` id, not a model name. The header label only skipped `agent_`, so it crossfaded the assistant's name into an internal id. Skip both prefixes, and offer `assistant.model` ahead of the message field in the callers that already resolved the assistant. The crossfade itself is pointer-only: the model span is `aria-hidden` and nothing in the label can take focus, so keyboard and screen reader users had no path to the value at all. Carry the model in text that never hides, which puts it in the header's accessible name alongside the author and the time. * refactor: Own the header crossfade in the component The provider-to-model crossfade lived in global CSS even though HeaderLabel is its only consumer. Tailwind expresses the whole effect: a named group for the hover scope, one grid cell shared by both labels, and the existing resize duration and easing variables. Reduced motion now follows the same motion-reduce convention as the rest of the client. * fix: Return a defined model name from the header lookup Array.find over nullable candidates widens the return to include null, which tsc rejects against the declared string | undefined. Narrow with a predicate and sort the imports the pre-commit hook rewrote. * fix: Keep the model reachable by keyboard and the scroll button inert The header crossfade was pointer-only, so a sighted keyboard user never saw the model name; the screen-reader copy covered announcement but not sight. Focusing anything in the message row now swaps the label too, the same hook the timestamp already reveals itself with. The scroll-to-bottom wrapper spans the column and stays inert so it never swallows clicks meant for the thread, which left the button to opt back into pointer events. A descendant that opts in is hit-testable however its parent paints, so the transition classes could not hold the control inert as they claimed: the button took clicks while invisible. Gate the opt-in on the enter transition settling and drop the declarations that never applied. * fix: Measure the scrollbar gutter and keep the scroll button unreachable The spacer assumed the gutter was the `::-webkit-scrollbar` width. Blink and WebKit honour that rule, Firefox ignores it and sizes the band itself, and an overlay scrollbar reserves nothing at all, so on those the composer and the scroll-to-bottom control sat off the messages they are supposed to line up with. Measure what the message column actually holds back and publish it for the spacer to read, leaving the token as the pre-measurement fallback. Gating the scroll button on pointer events alone also left it enabled, so it kept its place in the tab order and answered Enter while invisible. Disable it until the same gate opens, and hold its opacity so being briefly unreachable does not dim it on top of the wrapper's own fade. --- client/src/components/Chat/ChatView.tsx | 1 + .../Messages/Content/Parts/AuthorHeader.tsx | 25 ++-- .../Chat/Messages/Content/Parts/SteerPart.tsx | 2 +- .../Parts/__tests__/AuthorHeader.spec.tsx | 6 +- .../Parts/__tests__/SteerPart.test.tsx | 6 +- .../src/components/Chat/Messages/Message.tsx | 2 +- .../components/Chat/Messages/MessageParts.tsx | 9 +- .../components/Chat/Messages/MessagesView.tsx | 10 +- .../Chat/Messages/SearchMessage.tsx | 4 +- .../Chat/Messages/ui/HeaderLabel.tsx | 68 ++++++++++ .../Chat/Messages/ui/MessageRender.tsx | 7 + .../Chat/Messages/ui/MessageRow.tsx | 21 +-- .../Chat/Messages/ui/MessageTimestamp.tsx | 57 +++++++-- .../ui/__tests__/HeaderLabel.spec.tsx | 58 +++++++++ .../Messages/ui/__tests__/MessageRow.spec.tsx | 58 +++++++-- .../Chat/__tests__/ChatView.spec.tsx | 21 +++ .../src/components/Messages/ContentRender.tsx | 7 + .../components/Messages/MessageContent.tsx | 2 +- .../components/Messages/ScrollToBottom.tsx | 47 +++++-- .../__tests__/ScrollToBottom.spec.tsx | 121 ++++++++++++++---- client/src/components/Share/Message.tsx | 4 +- .../__tests__/useScrollbarGutter.spec.ts | 46 +++++++ client/src/hooks/Messages/index.ts | 1 + .../src/hooks/Messages/useScrollbarGutter.ts | 41 ++++++ client/src/locales/en/translation.json | 1 + client/src/mobile.css | 14 ++ client/src/style.css | 107 ++++++++-------- 27 files changed, 600 insertions(+), 146 deletions(-) create mode 100644 client/src/components/Chat/Messages/ui/HeaderLabel.tsx create mode 100644 client/src/components/Chat/Messages/ui/__tests__/HeaderLabel.spec.tsx create mode 100644 client/src/hooks/Messages/__tests__/useScrollbarGutter.spec.ts create mode 100644 client/src/hooks/Messages/useScrollbarGutter.ts diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx index d571a826f7..846719417e 100644 --- a/client/src/components/Chat/ChatView.tsx +++ b/client/src/components/Chat/ChatView.tsx @@ -139,6 +139,7 @@ function ChatView({ index = 0, project }: { index?: number; project?: TChatProje