diff --git a/packages/web-shell/client/components/MessageItem.dom.test.tsx b/packages/web-shell/client/components/MessageItem.dom.test.tsx index 1878636516..46362120eb 100644 --- a/packages/web-shell/client/components/MessageItem.dom.test.tsx +++ b/packages/web-shell/client/components/MessageItem.dom.test.tsx @@ -141,3 +141,26 @@ describe('MessageItem error isolation', () => { expect(alert.style.justifyContent).toBe('flex-start'); }); }); + +describe('MessageItem selectable wrapper', () => { + it('keeps the user-selectable wrapper out of layout via display: contents', () => { + // The wrapper only exists to carry the `data-user-selectable` CSS marker + // (standalone.css re-enables text selection through it). It must NOT + // generate a layout box: several parents are flex containers whose item + // used to be the message body itself — a plain div here becomes the flex + // item instead and shrinks to content width, squeezing the user chat + // bubble (max-width: 80% of the shrunken wrapper) so even short messages + // wrap mid-word. + const container = render( + {item(userMsg('1', 'hello'))}, + ); + const wrapper = container.querySelector( + '[data-user-selectable]', + ) as HTMLElement; + expect(wrapper).not.toBeNull(); + expect(wrapper.style.display).toBe('contents'); + // The message body renders inside the wrapper, so the CSS descendant + // selector `[data-user-selectable] *` still re-enables selection. + expect(wrapper.querySelector('[data-testid="user-ok"]')).not.toBeNull(); + }); +}); diff --git a/packages/web-shell/client/components/MessageItem.tsx b/packages/web-shell/client/components/MessageItem.tsx index 738d380878..a3bf0d5983 100644 --- a/packages/web-shell/client/components/MessageItem.tsx +++ b/packages/web-shell/client/components/MessageItem.tsx @@ -154,7 +154,20 @@ export const MessageItem = memo(function MessageItem({ // standalone.css disables selection on UI chrome (native-app feel); this // attribute opts the message subtree back in, including descendants // (Markdown body, code blocks, tool panels, sub-messages). - const selectableSafeBody =
{safeBody}
; + // + // `display: contents` keeps this wrapper out of layout: several parents + // (e.g. MessageTimestamp's chat row) are flex containers whose items used + // to be the message body itself. A plain div here becomes the flex item + // instead and shrinks to its content width, squeezing user chat bubbles + // (whose max-width: 80% then resolves against the shrunken wrapper) so + // even short messages wrap mid-word. The user-select re-enable rule + // matches `[data-user-selectable] *`, so the boxless wrapper does not + // affect it. + const selectableSafeBody = ( +
+ {safeBody} +
+ ); if (message.role === 'assistant') { if (showAssistantActions) {