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) {