fix(site/src/pages/AgentsPage/components): unify live thinking spacing and sizing (#25192)

This commit is contained in:
Danielle Maywood
2026-05-13 12:51:49 +01:00
committed by GitHub
parent 57b11d405f
commit b52c0bdb56
6 changed files with 95 additions and 4 deletions
@@ -480,6 +480,7 @@ const ChatMessageItem = memo<{
isAfterEditingMessage?: boolean;
hideActions?: boolean;
hasActiveStream?: boolean;
isAwaitingFirstStreamChunk?: boolean;
// When true, renders a gradient overlay inside the bubble
// that fades text out toward the bottom. Used by the sticky
@@ -505,6 +506,7 @@ const ChatMessageItem = memo<{
isAfterEditingMessage = false,
hideActions = false,
hasActiveStream = false,
isAwaitingFirstStreamChunk = false,
fadeFromBottom = false,
onImplementPlan,
onSendAskUserQuestionResponse,
@@ -528,6 +530,7 @@ const ChatMessageItem = memo<{
parsed,
hideActions,
hasActiveStream,
isAwaitingFirstStreamChunk,
});
if (displayState.shouldHide) {
return null;
@@ -965,6 +968,7 @@ function computeLastInChainFlags(
parsed: entry.parsed,
hideActions: false,
hasActiveStream: false,
isAwaitingFirstStreamChunk: false,
});
if (entry.message.role !== "user") {
flags[i] = nextVisibleIsUser;
@@ -993,6 +997,7 @@ interface ConversationTimelineProps {
mcpServers?: readonly TypesGen.MCPServerConfig[];
showDesktopPreviews?: boolean;
hasActiveStream?: boolean;
isAwaitingFirstStreamChunk?: boolean;
}
export const ConversationTimeline = memo<ConversationTimelineProps>(
@@ -1009,6 +1014,7 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
mcpServers,
showDesktopPreviews,
hasActiveStream,
isAwaitingFirstStreamChunk,
}) => {
const lastInChainFlags = computeLastInChainFlags(parsedMessages);
@@ -1110,6 +1116,7 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
isAfterEditingMessage={afterEditingMessageIds.has(message.id)}
hideActions={!isLastInChain}
hasActiveStream={Boolean(hasActiveStream)}
isAwaitingFirstStreamChunk={Boolean(isAwaitingFirstStreamChunk)}
mcpServers={mcpServers}
subagentTitles={subagentTitles}
subagentVariants={subagentVariants}
@@ -29,12 +29,12 @@ const hasTextOrReasoningBlock = (blocks: readonly RenderBlock[]): boolean =>
/**
* Placeholder shown during streaming before text or reasoning
* blocks arrive. Uses the same shimmer animation as the
* collapsible thinking disclosure label.
* blocks arrive. Uses the same shimmer animation and typography
* as the ChatStatusCallout status placeholder.
*/
const StreamingThinkingPlaceholder: FC = () => (
<div className="flex w-full items-center gap-2 py-0.5 text-content-secondary">
<Shimmer as="span" className="text-sm">
<Shimmer as="span" className="text-[13px] leading-relaxed">
Thinking
</Shimmer>
</div>
@@ -14,12 +14,16 @@ const buildMessage = (
content,
});
const getDisplayState = (message: ChatMessage) =>
const getDisplayState = (
message: ChatMessage,
overrides: Partial<Parameters<typeof deriveMessageDisplayState>[0]> = {},
) =>
deriveMessageDisplayState({
message,
parsed: parseMessageContent(message.content),
hideActions: false,
hasActiveStream: false,
...overrides,
});
describe("deriveMessageDisplayState", () => {
@@ -58,4 +62,55 @@ describe("deriveMessageDisplayState", () => {
expect(getDisplayState(message).hasCopyableContent).toBe(false);
});
it("shows the assistant spacer for reasoning messages when no suppressing flags apply", () => {
const message = buildMessage(
[{ type: "reasoning", text: "I should think before answering." }],
"assistant",
);
expect(getDisplayState(message).needsAssistantBottomSpacer).toBe(true);
});
it("suppresses the assistant spacer while awaiting the first stream chunk", () => {
const message = buildMessage(
[{ type: "reasoning", text: "I should think before answering." }],
"assistant",
);
expect(
getDisplayState(message, { isAwaitingFirstStreamChunk: true })
.needsAssistantBottomSpacer,
).toBe(false);
});
it("keeps the assistant spacer hidden when actions are hidden", () => {
const message = buildMessage(
[{ type: "reasoning", text: "I should think before answering." }],
"assistant",
);
expect(
getDisplayState(message, { hideActions: true })
.needsAssistantBottomSpacer,
).toBe(false);
});
it("keeps the assistant spacer hidden when a stream is active", () => {
const message = buildMessage(
[{ type: "reasoning", text: "I should think before answering." }],
"assistant",
);
expect(
getDisplayState(message, { hasActiveStream: true })
.needsAssistantBottomSpacer,
).toBe(false);
});
it("never shows the assistant spacer on user messages", () => {
const message = buildMessage([{ type: "text", text: "Hello" }], "user");
expect(getDisplayState(message).needsAssistantBottomSpacer).toBe(false);
});
});
@@ -42,11 +42,13 @@ export const deriveMessageDisplayState = ({
parsed,
hideActions,
hasActiveStream,
isAwaitingFirstStreamChunk = false,
}: {
message: TypesGen.ChatMessage;
parsed: ParsedMessageContent;
hideActions: boolean;
hasActiveStream: boolean;
isAwaitingFirstStreamChunk?: boolean;
}): MessageDisplayState => {
const isUser = message.role === "user";
const userInlineContent = isUser
@@ -66,6 +68,7 @@ export const deriveMessageDisplayState = ({
const needsAssistantBottomSpacer =
!hideActions &&
!hasActiveStream &&
!isAwaitingFirstStreamChunk &&
!isUser &&
!hasCopyableContent &&
(Boolean(parsed.reasoning) ||
@@ -99,6 +99,26 @@ export const StreamingToolCallGapRegression: Story = {
},
};
export const StartingPhaseToolCallGapRegression: Story = {
render: () => {
const store = buildRegressionStore();
store.setChatStatus("running");
return (
<ChatPageTimeline
chatID={CHAT_ID}
store={store}
persistedError={undefined}
/>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
canvas.getAllByText("Thinking...");
expect(canvas.queryByTestId("assistant-bottom-spacer")).toBeNull();
},
};
export const SpacerVisibleWhenNotStreaming: Story = {
render: () => {
const store = buildRegressionStore();
@@ -22,6 +22,7 @@ import { getLatestContextUsage } from "./ChatConversation/chatHelpers";
import {
selectChatStatus,
selectHasStreamState,
selectIsAwaitingFirstStreamChunk,
selectMessagesByID,
selectOrderedMessageIDs,
selectQueuedMessages,
@@ -75,6 +76,10 @@ export const ChatPageTimeline: FC<ChatPageTimelineProps> = ({
const orderedMessageIDs = useChatSelector(store, selectOrderedMessageIDs);
const chatStatus = useChatSelector(store, selectChatStatus);
const hasStream = useChatSelector(store, selectHasStreamState);
const isAwaitingFirstStreamChunk = useChatSelector(
store,
selectIsAwaitingFirstStreamChunk,
);
const isChatCompleted = !hasStream && chatStatus !== "pending";
const messages = orderedMessageIDs
@@ -119,6 +124,7 @@ export const ChatPageTimeline: FC<ChatPageTimelineProps> = ({
onSendAskUserQuestionResponse={onSendAskUserQuestionResponse}
isChatCompleted={isChatCompleted}
hasActiveStream={hasStream}
isAwaitingFirstStreamChunk={isAwaitingFirstStreamChunk}
urlTransform={urlTransform}
mcpServers={mcpServers}
showDesktopPreviews={false}