Files
fusion/packages/dashboard/app/components/__tests__/StandardChatSurface.empty-message.test.tsx
gsxdsm 70330bcffb FN-7779: render No message placeholder for empty assistant chat replies
Empty final assistant chat messages (e.g. Grok CLI runs that finish with no text) now show a muted "No message" placeholder instead of a blank bubble, in both Chat and Planner Chat via the shared StandardChatSurface component.

- Add isEmptyAssistantMessage check in StandardChatMessageItem: only applies to final persisted assistant messages with no content, tool calls, thinking output, attachments, or failure info
- Render a new .chat-message-content--empty block with the "No message" i18n string when the check matches
- Add shared CSS rule combining --waiting and --empty muted/italic styling, switching to var(--font-size-sm)
- Add "chat.noMessage" translation key to en/app.json and regenerate resources.d.ts
- Document the new behavior in docs/dashboard-guide.md
- Add a patch changeset for @runfusion/fusion
- Add StandardChatSurface.empty-message.test.tsx covering empty/whitespace content, populated content, tool calls, thinking output, attachments, failure info, non-assistant roles, and streaming waiting/thinking states

Files changed:
$(cat /tmp/diffstat_fn7779.txt)

Fusion-Task-Id: FN-7779

Fusion-Task-Lineage: d3ae068e-acba-4b4c-85d6-a788aa24a53b

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-10 07:40:22 -07:00

158 lines
5.1 KiB
TypeScript

import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { StandardChatMessageItem, StandardStreamingMessage } from "../StandardChatSurface";
import type { ChatMessageInfo, ToolCallInfo } from "../../hooks/chatTypes";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (_key: string, fallback?: string) => fallback ?? _key,
}),
}));
const baseMessage: ChatMessageInfo = {
id: "msg-assistant-empty",
sessionId: "session-1",
role: "assistant",
content: "",
createdAt: "2026-07-10T00:00:00.000Z",
};
function renderMessage(overrides: Partial<ChatMessageInfo> = {}, forcePlain = false) {
const message: ChatMessageInfo = { ...baseMessage, ...overrides };
return render(
<StandardChatMessageItem
message={message}
forcePlain={forcePlain}
agentName="Assistant"
hideAssistantIdentity={false}
showAssistantModelTag={false}
activeModelTag={null}
activeModelProvider={null}
activeSessionId="session-1"
/>,
);
}
function expectNoPlaceholder() {
expect(screen.queryByTestId("chat-message-empty")).not.toBeInTheDocument();
expect(screen.queryByText("No message")).not.toBeInTheDocument();
}
describe("StandardChatSurface empty assistant messages", () => {
afterEach(() => {
cleanup();
});
it.each([
{ name: "empty markdown", content: "", forcePlain: false },
{ name: "empty plain", content: "", forcePlain: true },
{ name: "whitespace markdown", content: " \n", forcePlain: false },
{ name: "whitespace plain", content: " \n", forcePlain: true },
])("renders No message for $name assistant content", ({ content, forcePlain }) => {
renderMessage({ content }, forcePlain);
expect(screen.getByTestId("chat-message-empty")).toHaveTextContent("No message");
});
it("does not render the placeholder for populated assistant content", () => {
renderMessage({ content: "Hello **there**" });
expectNoPlaceholder();
expect(screen.getByText("Hello")).toBeInTheDocument();
});
it("does not render the placeholder when tool calls are the assistant content", () => {
const toolCalls: ToolCallInfo[] = [
{ toolName: "read_file", status: "completed", isError: false, result: "done" },
];
renderMessage({ content: "", toolCalls });
expectNoPlaceholder();
expect(screen.getByText("read_file")).toBeInTheDocument();
});
it("does not render the placeholder when thinking output is present", () => {
renderMessage({ content: "", thinkingOutput: "Reasoning through the request" });
expectNoPlaceholder();
expect(screen.getByText("Thinking")).toBeInTheDocument();
expect(screen.getByText("Reasoning through the request")).toBeInTheDocument();
});
it("does not render the placeholder when attachments are present", () => {
renderMessage({
content: "",
attachments: [
{
id: "attachment-1",
filename: "artifact.txt",
originalName: "artifact.txt",
mimeType: "text/plain",
size: 12,
createdAt: "2026-07-10T00:00:00.000Z",
},
],
});
expectNoPlaceholder();
expect(screen.getByTestId("chat-message-attachment")).toHaveTextContent("artifact.txt");
});
it("does not render the placeholder when failure info is present", () => {
renderMessage({ content: "", failureInfo: { summary: "Provider failed" } });
expectNoPlaceholder();
expect(screen.getByText("Response failed")).toBeInTheDocument();
expect(screen.getByText("Provider failed")).toBeInTheDocument();
});
it.each([
{ role: "user" as const, content: "" },
{ role: "system" as const, content: "" },
])("does not render the assistant placeholder for $role messages", ({ role, content }) => {
renderMessage({ id: `msg-${role}`, role, content });
expectNoPlaceholder();
});
it("keeps the streaming waiting state separate from the empty final-message placeholder", () => {
render(
<StandardStreamingMessage
streamingText=""
streamingThinking=""
streamingToolCalls={[]}
forcePlain={false}
agentName="Assistant"
hideAssistantIdentity={false}
showAssistantModelTag={false}
activeModelTag={null}
activeModelProvider={null}
/>,
);
expect(screen.getByText("Working…")).toBeInTheDocument();
expect(document.querySelector(".chat-message-content--waiting")).toBeInTheDocument();
expectNoPlaceholder();
});
it("keeps the streaming thinking state separate from the empty final-message placeholder", () => {
render(
<StandardStreamingMessage
streamingText=""
streamingThinking="Thinking about it"
streamingToolCalls={[]}
forcePlain={false}
agentName="Assistant"
hideAssistantIdentity={false}
showAssistantModelTag={false}
activeModelTag={null}
activeModelProvider={null}
/>,
);
expect(screen.getByText("Thinking…")).toBeInTheDocument();
expect(screen.getByText("Thinking about it")).toBeInTheDocument();
expectNoPlaceholder();
});
});