diff --git a/frontend/src/components/Chat.test.tsx b/frontend/src/components/Chat.test.tsx
index d59f1ab..174fa22 100644
--- a/frontend/src/components/Chat.test.tsx
+++ b/frontend/src/components/Chat.test.tsx
@@ -892,3 +892,164 @@ describe("Chat message queue (Story 155)", () => {
});
});
});
+
+describe("Remove bubble styling from streaming messages (Story 163)", () => {
+ beforeEach(() => {
+ capturedWsHandlers = null;
+ setupMocks();
+ });
+
+ it("AC1: streaming assistant message uses transparent background, no extra padding, no border-radius", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ // Send a message to put chat into loading state
+ const input = screen.getByPlaceholderText("Send a message...");
+ await act(async () => {
+ fireEvent.change(input, { target: { value: "Hello" } });
+ });
+ await act(async () => {
+ fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
+ });
+
+ // Simulate streaming tokens arriving
+ act(() => {
+ capturedWsHandlers?.onToken("Streaming response text");
+ });
+
+ // Find the streaming message container (the inner div wrapping the Markdown)
+ const streamingText = await screen.findByText("Streaming response text");
+ // The markdown-body wrapper is the parent, and the styled div is its parent
+ const styledDiv = streamingText.closest(".markdown-body")
+ ?.parentElement as HTMLElement;
+
+ expect(styledDiv).toBeTruthy();
+ const styleAttr = styledDiv.getAttribute("style") ?? "";
+ expect(styleAttr).toContain("background: transparent");
+ expect(styleAttr).toContain("padding: 0px");
+ expect(styleAttr).toContain("border-radius: 0px");
+ expect(styleAttr).toContain("max-width: 100%");
+ });
+
+ it("AC1: streaming message wraps Markdown in markdown-body class", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ const input = screen.getByPlaceholderText("Send a message...");
+ await act(async () => {
+ fireEvent.change(input, { target: { value: "Hello" } });
+ });
+ await act(async () => {
+ fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
+ });
+
+ act(() => {
+ capturedWsHandlers?.onToken("Some markdown content");
+ });
+
+ const streamingText = await screen.findByText("Some markdown content");
+ const markdownBody = streamingText.closest(".markdown-body");
+ expect(markdownBody).toBeTruthy();
+ });
+
+ it("AC2: no visual change when streaming ends and message transitions to completed", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ // Send a message to start streaming
+ const input = screen.getByPlaceholderText("Send a message...");
+ await act(async () => {
+ fireEvent.change(input, { target: { value: "Hello" } });
+ });
+ await act(async () => {
+ fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
+ });
+
+ // Simulate streaming tokens
+ act(() => {
+ capturedWsHandlers?.onToken("Final response");
+ });
+
+ // Capture streaming message style attribute
+ const streamingText = await screen.findByText("Final response");
+ const streamingStyledDiv = streamingText.closest(".markdown-body")
+ ?.parentElement as HTMLElement;
+ const streamingStyleAttr = streamingStyledDiv.getAttribute("style") ?? "";
+
+ // Transition: onUpdate completes the message
+ act(() => {
+ capturedWsHandlers?.onUpdate([
+ { role: "user", content: "Hello" },
+ { role: "assistant", content: "Final response" },
+ ]);
+ });
+
+ // Find the completed message — it should have the same styling
+ const completedText = await screen.findByText("Final response");
+ const completedMarkdownBody = completedText.closest(".markdown-body");
+ const completedStyledDiv =
+ completedMarkdownBody?.parentElement as HTMLElement;
+
+ expect(completedStyledDiv).toBeTruthy();
+ const completedStyleAttr = completedStyledDiv.getAttribute("style") ?? "";
+
+ // Both streaming and completed use transparent bg, 0 padding, 0 border-radius
+ expect(completedStyleAttr).toContain("background: transparent");
+ expect(completedStyleAttr).toContain("padding: 0px");
+ expect(completedStyleAttr).toContain("border-radius: 0px");
+ expect(streamingStyleAttr).toContain("background: transparent");
+ expect(streamingStyleAttr).toContain("padding: 0px");
+ expect(streamingStyleAttr).toContain("border-radius: 0px");
+
+ // Both have the markdown-body class wrapper
+ expect(streamingStyledDiv.querySelector(".markdown-body")).toBeTruthy();
+ });
+
+ it("AC3: completed assistant messages retain transparent background and no border-radius", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ act(() => {
+ capturedWsHandlers?.onUpdate([
+ { role: "user", content: "Hi" },
+ { role: "assistant", content: "Hello there!" },
+ ]);
+ });
+
+ const assistantText = await screen.findByText("Hello there!");
+ const markdownBody = assistantText.closest(".markdown-body");
+ const styledDiv = markdownBody?.parentElement as HTMLElement;
+
+ expect(styledDiv).toBeTruthy();
+ const styleAttr = styledDiv.getAttribute("style") ?? "";
+ expect(styleAttr).toContain("background: transparent");
+ expect(styleAttr).toContain("padding: 0px");
+ expect(styleAttr).toContain("border-radius: 0px");
+ expect(styleAttr).toContain("max-width: 100%");
+ });
+
+ it("AC3: completed user messages still have their bubble styling", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ act(() => {
+ capturedWsHandlers?.onUpdate([
+ { role: "user", content: "I am a user message" },
+ { role: "assistant", content: "I am a response" },
+ ]);
+ });
+
+ // findByText returns the styled div itself for user messages (text is direct child)
+ const userStyledDiv = await screen.findByText("I am a user message");
+ const styleAttr = userStyledDiv.getAttribute("style") ?? "";
+ // User messages retain bubble: distinct background, padding, rounded corners
+ expect(styleAttr).toContain("padding: 10px 16px");
+ expect(styleAttr).toContain("border-radius: 20px");
+ expect(styleAttr).not.toContain("background: transparent");
+ });
+});
diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx
index 3a385da..c9ea9b4 100644
--- a/frontend/src/components/Chat.tsx
+++ b/frontend/src/components/Chat.tsx
@@ -761,45 +761,49 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
>
-
{
- const match = /language-(\w+)/.exec(className || "");
- const isInline = !className;
- return !isInline && match ? (
-
- {String(children).replace(/\n$/, "")}
-
- ) : (
-
- {children}
-
- );
- },
- }}
- >
- {streamingContent}
-
+
+ {
+ const match = /language-(\w+)/.exec(
+ className || "",
+ );
+ const isInline = !className;
+ return !isInline && match ? (
+
+ {String(children).replace(/\n$/, "")}
+
+ ) : (
+
+ {children}
+
+ );
+ },
+ }}
+ >
+ {streamingContent}
+
+
)}