diff --git a/packages/chat-ui/src/styles.css b/packages/chat-ui/src/styles.css
index c27f4b5d9..0902d567b 100644
--- a/packages/chat-ui/src/styles.css
+++ b/packages/chat-ui/src/styles.css
@@ -2862,6 +2862,16 @@
margin-left: 0;
}
+/* Message body column: bubble/parts + reaction/pin row share one flex
+ column so chips sit under the bubble instead of as a loose sibling of
+ the whole header+bubble cluster (CL-6661). */
+.chat-message-body {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ max-width: 100%;
+}
+
/* Existing reaction chips + pin toggle, indented to align under the bubble
text the same way `.chat-thread-affordance` does. Chips are content —
they always show once a reaction exists. The pin toggle fades in on
@@ -2872,7 +2882,7 @@
display: flex;
align-items: center;
gap: 0.35rem;
- margin: -0.1rem 0 0.2rem 2.9rem;
+ margin: 0.05rem 0 0.15rem 2.9rem;
max-width: 100%;
}
diff --git a/packages/chat-ui/src/timeline.tsx b/packages/chat-ui/src/timeline.tsx
index baac298bf..a8e457ccb 100644
--- a/packages/chat-ui/src/timeline.tsx
+++ b/packages/chat-ui/src/timeline.tsx
@@ -1429,139 +1429,145 @@ function MessagePartsInner({
>
{showDayDivider && }
- {groupTimelineParts(item.parts, groupKey).map((group) => {
- const key = group.key;
- if (group.kind === "tool-activity") {
- return
;
- }
- const part = group.part;
- if (part.kind === "text" && part.turnFailed === true) {
- const retryText = findRetryText(items, item);
- return (
-
- );
- }
- if (part.kind === "text") {
- return (
-
- );
- }
- if (part.kind === "event") {
- return (
-
- );
- }
- if (part.kind === "file") {
- return (
-
- );
- }
- // The agent's own thinking still renders through react-ui, which
- // owns the reasoning disclosure. Tool calls no longer do: they
- // arrive here already folded into rounds by `groupTimelineParts`
- // above, and react-ui's `ToolBlock` renders one call at a time
- // with its arguments and result as `JSON.stringify` output.
- if (part.kind === "reasoning") {
- return (
-
- );
- }
- if (part.kind === "block") {
- return (
-
- );
- }
- return
;
- })}
- {(() => {
- const hasReactions =
- offersSocialChrome &&
- reactionActions !== undefined &&
- (item.reactions?.length ?? 0) > 0;
- // Unpinned messages offer no persistent glyph here — pinning
- // itself stays reachable through the ellipsis menu's own
- // "Pin"/"Unpin" entry (`buildMessageMenu`); this row only shows
- // once there's something to show (a reaction, or a message
- // already pinned, which needs a visible way to unpin). Before
- // this, a pin toggle mounted for every message the moment a host
- // wired `pinActions` at all, CSS-hidden until hover but present
- // in the DOM under every line, greeting included. System / error
- // / connect rows (CL-6739) never show this cluster either.
- const isPinned =
- offersSocialChrome &&
- pinActions !== undefined &&
- item.pinned === true;
- if (!hasReactions && !isPinned) return null;
- return (
-
- {hasReactions && reactionActions !== undefined ? (
-
+ {groupTimelineParts(item.parts, groupKey).map((group) => {
+ const key = group.key;
+ if (group.kind === "tool-activity") {
+ return ;
+ }
+ const part = group.part;
+ if (part.kind === "text" && part.turnFailed === true) {
+ const retryText = findRetryText(items, item);
+ return (
+
+ );
+ }
+ if (part.kind === "text") {
+ return (
+
+ );
+ }
+ if (part.kind === "event") {
+ return (
+
+ );
+ }
+ if (part.kind === "file") {
+ return (
+
- ) : null}
- {isPinned && pinActions !== undefined ? (
-
+ );
+ }
+ if (part.kind === "block") {
+ return (
+
- ) : null}
-
- );
- })()}
+ );
+ }
+ return
;
+ })}
+ {(() => {
+ const hasReactions =
+ offersSocialChrome &&
+ reactionActions !== undefined &&
+ (item.reactions?.length ?? 0) > 0;
+ // Unpinned messages offer no persistent glyph here — pinning
+ // itself stays reachable through the ellipsis menu's own
+ // "Pin"/"Unpin" entry (`buildMessageMenu`); this row only shows
+ // once there's something to show (a reaction, or a message
+ // already pinned, which needs a visible way to unpin). Before
+ // this, a pin toggle mounted for every message the moment a host
+ // wired `pinActions` at all, CSS-hidden until hover but present
+ // in the DOM under every line, greeting included. System / error
+ // / connect rows (CL-6739) never show this cluster either.
+ const isPinned =
+ offersSocialChrome &&
+ pinActions !== undefined &&
+ item.pinned === true;
+ if (!hasReactions && !isPinned) return null;
+ return (
+
+ {hasReactions && reactionActions !== undefined ? (
+
+ ) : null}
+ {isPinned && pinActions !== undefined ? (
+
+ ) : null}
+
+ );
+ })()}
+
{offersSocialChrome && onOpenThread !== undefined && replyCount > 0 ? (