fix: fix 4px layout shift on streaming commit in chat (#24203)

Closes CODAGT-124

When a streaming assistant response finishes and moves from the live
stream
tail into the conversation timeline, the message jumps 4px upward. This
happens because the outer layout wrapper and live-stream section both
used
`gap-3` (12px), while the committed-message list used `gap-2` (8px).

Unify all three containers to `gap-2` so the gap between messages stays
at 8px regardless of whether they're streaming or committed, eliminating
the layout shift.

A Storybook story with play-function assertions locks the invariant: it
renders both committed messages and an active stream, then verifies both
the outer and inner containers report `rowGap === "8px"`.
This commit is contained in:
Jaayden Halko
2026-04-10 19:09:03 +07:00
committed by GitHub
parent 4018320614
commit 1a3a92bd1b
4 changed files with 36 additions and 3 deletions
@@ -474,6 +474,16 @@ const buildStoreWithMessages = (
return store;
};
const gapTestStore = createChatStore();
gapTestStore.replaceMessages([
buildMessage(1, "user", "Explain the layout."),
buildMessage(2, "assistant", "Here is the explanation."),
buildMessage(3, "user", "Can you elaborate?"),
]);
gapTestStore.applyMessageParts([
{ type: "text", text: "Certainly, here are more details..." },
]);
// ---------------------------------------------------------------------------
// Editing flow stories
// ---------------------------------------------------------------------------
@@ -521,6 +531,26 @@ export const EditingSaving: Story = {
),
};
export const ConsistentGapBetweenTimelineAndStream: Story = {
render: () => <StoryAgentChatPageView store={gapTestStore} />,
play: async ({ canvasElement }) => {
const wrapper = canvasElement.querySelector(
'[data-testid="chat-timeline-wrapper"]',
);
expect(wrapper).not.toBeNull();
const outerGap = window.getComputedStyle(wrapper!).rowGap;
expect(outerGap).toBe("8px");
const timeline = wrapper!.querySelector(
'[data-testid="conversation-timeline"]',
);
expect(timeline).not.toBeNull();
const innerGap = window.getComputedStyle(timeline!).rowGap;
expect(innerGap).toBe("8px");
},
};
// ---------------------------------------------------------------------------
// AgentChatPageNotFoundView stories
// ---------------------------------------------------------------------------
@@ -1041,7 +1041,7 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
}
return (
<div className="flex flex-col gap-2">
<div data-testid="conversation-timeline" className="flex flex-col gap-2">
{parsedMessages.map(({ message, parsed }, msgIdx) => {
if (message.role === "user") {
return (
@@ -70,7 +70,7 @@ export const LiveStreamTailContent = ({
}
return (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
{shouldRenderEmptyState && (
<div className="py-12 text-center text-content-secondary">
<p className="text-sm">Start a conversation with your agent.</p>
@@ -86,7 +86,10 @@ export const ChatPageTimeline: FC<ChatPageTimelineProps> = ({
return (
<Profiler id="AgentChat" onRender={onRenderProfiler}>
<div className="mx-auto flex w-full max-w-3xl flex-col gap-3 py-6">
<div
data-testid="chat-timeline-wrapper"
className="mx-auto flex w-full max-w-3xl flex-col gap-2 py-6"
>
{/* VNC sessions for completed agents may already be
terminated, so inline desktop previews are disabled
via showDesktopPreviews={false} to avoid a perpetual