From 76d89f59af42ca605bcc1c506c019ffcd7fec1fa Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Fri, 10 Apr 2026 19:09:23 +0700 Subject: [PATCH] fix(site): add bottom spacing for sources-only assistant messages (#24202) Closes CODAGT-123 Assistant messages containing only source parts (no markdown or reasoning) were missing the bottom spacer that normally fills the gap left by the hidden action bar, causing them to sit flush against the next user bubble. The existing fallback spacer guarded on `Boolean(parsed.reasoning)`, so it only fired for thinking-only replies. Replace that guard with the broader `hasRenderableContent` flag (which covers blocks, tools, and sources) and extract a named `needsAssistantBottomSpacer` boolean so future content types inherit consistent spacing without re-reading compound conditions. Adds a `SourcesOnlyAssistantSpacing` Storybook story mirroring the existing `ThinkingOnlyAssistantSpacing` pattern for regression coverage. --- .../ConversationTimeline.stories.tsx | 56 +++++++++++++++++++ .../ChatConversation/ConversationTimeline.tsx | 14 +++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx index 210bfe4ce1..66b2e9cee9 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx @@ -939,3 +939,59 @@ export const ThinkingOnlyAssistantSpacing: Story = { expect(canvas.getByText("Any progress?")).toBeInTheDocument(); }, }; + +/** + * Regression: sources-only assistant messages must have consistent + * bottom spacing before the next user bubble. A spacer div fills the + * gap that would normally come from the hidden action bar. + */ +export const SourcesOnlyAssistantSpacing: Story = { + args: { + ...defaultArgs, + parsedMessages: buildMessages([ + { + ...baseMessage, + id: 1, + role: "user", + content: [{ type: "text", text: "Can you share your sources?" }], + }, + { + ...baseMessage, + id: 2, + role: "assistant", + content: [ + { + type: "source", + url: "https://example.com/docs", + title: "Documentation", + }, + { + type: "source", + url: "https://example.com/api", + title: "API Reference", + }, + ], + }, + { + ...baseMessage, + id: 3, + role: "user", + content: [{ type: "text", text: "Thanks!" }], + }, + ]), + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText("Can you share your sources?")).toBeInTheDocument(); + expect(canvas.getByText("Thanks!")).toBeInTheDocument(); + await userEvent.click( + canvas.getByRole("button", { name: /searched 2 results/i }), + ); + expect( + canvas.getByRole("link", { name: "Documentation" }), + ).toBeInTheDocument(); + expect( + canvas.getByRole("link", { name: "API Reference" }), + ).toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx index 0db14d35dd..2992c1b4dc 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx @@ -516,6 +516,11 @@ const ChatMessageItem = memo<{ userInlineContent.length > 0 || Boolean(parsed.markdown?.trim()); const hasFileBlocks = userFileBlocks.length > 0; const hasCopyableContent = Boolean(parsed.markdown.trim()); + const needsAssistantBottomSpacer = + !hideActions && + !isUser && + !hasCopyableContent && + (Boolean(parsed.reasoning) || parsed.sources.length > 0); const conversationItemProps: { role: "user" | "assistant" } = { role: isUser ? "user" : "assistant", @@ -670,12 +675,9 @@ const ChatMessageItem = memo<{ )} {/* Spacer for assistant messages without an action bar - (e.g. thinking-only) so they have consistent bottom - padding before the next user bubble. */} - {!hideActions && - !isUser && - !hasCopyableContent && - Boolean(parsed.reasoning) &&
} + (e.g. reasoning-only or sources-only) so they have + consistent bottom padding before the next user bubble. */} + {needsAssistantBottomSpacer &&
} {previewImage && (