mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
fix(site/src/pages/AgentsPage): suppress last-message spacer during active stream (#25120)
This commit is contained in:
@@ -479,6 +479,7 @@ const ChatMessageItem = memo<{
|
||||
editingMessageId?: number | null;
|
||||
isAfterEditingMessage?: boolean;
|
||||
hideActions?: boolean;
|
||||
hasActiveStream?: boolean;
|
||||
|
||||
// When true, renders a gradient overlay inside the bubble
|
||||
// that fades text out toward the bottom. Used by the sticky
|
||||
@@ -503,6 +504,7 @@ const ChatMessageItem = memo<{
|
||||
editingMessageId,
|
||||
isAfterEditingMessage = false,
|
||||
hideActions = false,
|
||||
hasActiveStream = false,
|
||||
fadeFromBottom = false,
|
||||
onImplementPlan,
|
||||
onSendAskUserQuestionResponse,
|
||||
@@ -525,6 +527,7 @@ const ChatMessageItem = memo<{
|
||||
message,
|
||||
parsed,
|
||||
hideActions,
|
||||
hasActiveStream,
|
||||
});
|
||||
if (displayState.shouldHide) {
|
||||
return null;
|
||||
@@ -961,6 +964,7 @@ function computeLastInChainFlags(
|
||||
message: entry.message,
|
||||
parsed: entry.parsed,
|
||||
hideActions: false,
|
||||
hasActiveStream: false,
|
||||
});
|
||||
if (entry.message.role !== "user") {
|
||||
flags[i] = nextVisibleIsUser;
|
||||
@@ -988,7 +992,7 @@ interface ConversationTimelineProps {
|
||||
urlTransform?: UrlTransform;
|
||||
mcpServers?: readonly TypesGen.MCPServerConfig[];
|
||||
showDesktopPreviews?: boolean;
|
||||
isTurnActive?: boolean;
|
||||
hasActiveStream?: boolean;
|
||||
}
|
||||
|
||||
export const ConversationTimeline = memo<ConversationTimelineProps>(
|
||||
@@ -1004,6 +1008,7 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
|
||||
urlTransform,
|
||||
mcpServers,
|
||||
showDesktopPreviews,
|
||||
hasActiveStream,
|
||||
}) => {
|
||||
const lastInChainFlags = computeLastInChainFlags(parsedMessages);
|
||||
|
||||
@@ -1104,6 +1109,7 @@ export const ConversationTimeline = memo<ConversationTimelineProps>(
|
||||
urlTransform={urlTransform}
|
||||
isAfterEditingMessage={afterEditingMessageIds.has(message.id)}
|
||||
hideActions={!isLastInChain}
|
||||
hasActiveStream={Boolean(hasActiveStream)}
|
||||
mcpServers={mcpServers}
|
||||
subagentTitles={subagentTitles}
|
||||
subagentVariants={subagentVariants}
|
||||
|
||||
@@ -42,10 +42,12 @@ export const deriveMessageDisplayState = ({
|
||||
message,
|
||||
parsed,
|
||||
hideActions,
|
||||
hasActiveStream,
|
||||
}: {
|
||||
message: TypesGen.ChatMessage;
|
||||
parsed: ParsedMessageContent;
|
||||
hideActions: boolean;
|
||||
hasActiveStream: boolean;
|
||||
}): MessageDisplayState => {
|
||||
const isUser = message.role === "user";
|
||||
const userInlineContent = isUser
|
||||
@@ -64,6 +66,7 @@ export const deriveMessageDisplayState = ({
|
||||
parsed.sources.length > 0;
|
||||
const needsAssistantBottomSpacer =
|
||||
!hideActions &&
|
||||
!hasActiveStream &&
|
||||
!isUser &&
|
||||
!hasCopyableContent &&
|
||||
(Boolean(parsed.reasoning) ||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, within } from "storybook/test";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { createChatStore } from "./ChatConversation/chatStore";
|
||||
import {
|
||||
buildStreamRenderState,
|
||||
FIXTURE_NOW,
|
||||
} from "./ChatConversation/storyFixtures";
|
||||
import { ChatPageTimeline } from "./ChatPageContent";
|
||||
|
||||
const meta = {
|
||||
title: "pages/AgentsPage/ChatPageContent",
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const CHAT_ID = "chat-page-content-stories";
|
||||
|
||||
const buildMessage = (
|
||||
id: number,
|
||||
role: TypesGen.ChatMessageRole,
|
||||
content: TypesGen.ChatMessagePart[],
|
||||
): TypesGen.ChatMessage => ({
|
||||
id,
|
||||
chat_id: CHAT_ID,
|
||||
created_at: new Date(FIXTURE_NOW - (10 - id) * 60_000).toISOString(),
|
||||
role,
|
||||
content,
|
||||
});
|
||||
|
||||
const buildRegressionStore = () => {
|
||||
const store = createChatStore();
|
||||
|
||||
store.replaceMessages([
|
||||
buildMessage(1, "user", [{ type: "text", text: "Read the source files" }]),
|
||||
buildMessage(2, "assistant", [
|
||||
{
|
||||
type: "reasoning",
|
||||
text: "I should read SKILL.md and main.go to understand the codebase.",
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
tool_call_id: "tool-1",
|
||||
tool_name: "read_file",
|
||||
args: { path: "SKILL.md" },
|
||||
},
|
||||
{
|
||||
type: "tool-call",
|
||||
tool_call_id: "tool-2",
|
||||
tool_name: "read_file",
|
||||
args: { path: "main.go" },
|
||||
},
|
||||
]),
|
||||
buildMessage(3, "tool", [
|
||||
{
|
||||
type: "tool-result",
|
||||
tool_call_id: "tool-1",
|
||||
result: { output: "# SKILL.md contents" },
|
||||
},
|
||||
]),
|
||||
buildMessage(4, "tool", [
|
||||
{
|
||||
type: "tool-result",
|
||||
tool_call_id: "tool-2",
|
||||
result: { output: "package main" },
|
||||
},
|
||||
]),
|
||||
]);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
export const StreamingToolCallGapRegression: Story = {
|
||||
render: () => {
|
||||
const store = buildRegressionStore();
|
||||
const { streamState } = buildStreamRenderState([
|
||||
{
|
||||
type: "tool-call",
|
||||
tool_call_id: "tool-streaming",
|
||||
tool_name: "read_file",
|
||||
args: { path: "types.go" },
|
||||
},
|
||||
]);
|
||||
store.setStreamState(streamState);
|
||||
store.setChatStatus("pending");
|
||||
|
||||
return (
|
||||
<ChatPageTimeline
|
||||
chatID={CHAT_ID}
|
||||
store={store}
|
||||
persistedError={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.queryByTestId("assistant-bottom-spacer")).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
export const SpacerVisibleWhenNotStreaming: Story = {
|
||||
render: () => {
|
||||
const store = buildRegressionStore();
|
||||
|
||||
return (
|
||||
<ChatPageTimeline
|
||||
chatID={CHAT_ID}
|
||||
store={store}
|
||||
persistedError={undefined}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
expect(canvas.getByTestId("assistant-bottom-spacer")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -118,6 +118,7 @@ export const ChatPageTimeline: FC<ChatPageTimelineProps> = ({
|
||||
onImplementPlan={onImplementPlan}
|
||||
onSendAskUserQuestionResponse={onSendAskUserQuestionResponse}
|
||||
isChatCompleted={isChatCompleted}
|
||||
hasActiveStream={hasStream}
|
||||
urlTransform={urlTransform}
|
||||
mcpServers={mcpServers}
|
||||
showDesktopPreviews={false}
|
||||
|
||||
Reference in New Issue
Block a user