mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site/src/pages/AgentsPage/components): unify live thinking spacing and sizing (#25192)
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user