webui: fix mobile error message styling to display in red

Add proper red styling for error messages in mobile chat interface to match
desktop behavior and fix GitHub issue #141.

Problem Analysis:
Mobile error messages lacked visual distinction from regular messages, appearing
with the same gray background as assistant messages. The desktop interface
properly displays error messages with red background (#ffebee) and red text
(#d32f2f), but the mobile-chat component was missing this styling, creating
inconsistent user experience across interfaces.

The mobile chat component correctly filtered error messages (type: 'error')
and displayed them, but the getMessageRole() method didn't handle error types,
defaulting them to 'assistant' styling.

Implementation Changes:

1. CSS Error Message Styling:
   - Added .message.error .message-bubble styles with red theme
   - Background: #ffebee (light red background matching desktop)
   - Text color: #d32f2f (dark red text for readability)
   - Border-radius: 18px (uniform rounding for clean appearance)
   - Maintains mobile-optimized layout and touch-friendly design

2. Message Role Classification:
   - Enhanced getMessageRole() method to properly handle error message types
   - Added explicit 'error' case returning 'error' class name
   - Ensures error messages receive proper CSS class assignment

3. Test Coverage:
   - Added comprehensive mobile-chat component tests
   - Specific test for error message red styling verification
   - CSS color validation using computed styles
   - Message filtering and rendering tests for all message types

Technical Details:
- Styling follows existing mobile component patterns with touch-friendly design
- Color scheme matches desktop timeline-message component for consistency
- Error messages maintain left alignment like other system messages
- Clean, uniformly rounded corners without extra borders for polished appearance

Benefits:
- Visual consistency between desktop and mobile error message presentation
- Clear error message identification for mobile users
- Improved accessibility with distinct error styling
- Maintains touch-friendly mobile design principles
- Clean, professional appearance without visual clutter
- Resolves GitHub issue #141 mobile error visibility

Testing:
- Added mobile-chat.test.ts with error message styling verification
- TypeScript compilation successful with no type errors
- Build process completes without warnings
- CSS computed style validation confirms red color application

This fix ensures error messages are visually distinctive on mobile devices,
providing users with clear feedback when errors occur while maintaining
the clean, touch-optimized mobile interface design.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s8b70f2efae56458ck
diff --git a/webui/src/web-components/mobile-chat.test.ts b/webui/src/web-components/mobile-chat.test.ts
new file mode 100644
index 0000000..54ddf2e
--- /dev/null
+++ b/webui/src/web-components/mobile-chat.test.ts
@@ -0,0 +1,172 @@
+import { test, expect } from "@sand4rt/experimental-ct-web";
+import { MobileChat } from "./mobile-chat";
+import { AgentMessage } from "../types";
+
+// Helper function to create mock messages
+function createMockMessage(props: Partial<AgentMessage> = {}): AgentMessage {
+  return {
+    idx: props.idx || 0,
+    type: props.type || "agent",
+    content: props.content || "Hello world",
+    timestamp: props.timestamp || "2023-05-15T12:00:00Z",
+    elapsed: props.elapsed || 1500000000, // 1.5 seconds in nanoseconds
+    end_of_turn: props.end_of_turn || false,
+    conversation_id: props.conversation_id || "conv123",
+    ...props,
+  };
+}
+
+test("renders basic chat messages", async ({ mount }) => {
+  const messages = [
+    createMockMessage({
+      type: "user",
+      content: "Hello, this is a user message",
+    }),
+    createMockMessage({
+      type: "agent",
+      content: "Hello, this is an agent response",
+    }),
+  ];
+
+  const component = await mount(MobileChat, {
+    props: {
+      messages: messages,
+    },
+  });
+
+  await expect(component.locator(".message.user")).toBeVisible();
+  await expect(component.locator(".message.assistant")).toBeVisible();
+  await expect(
+    component.locator(".message.user .message-bubble"),
+  ).toContainText("Hello, this is a user message");
+  await expect(
+    component.locator(".message.assistant .message-bubble"),
+  ).toContainText("Hello, this is an agent response");
+});
+
+test("renders error messages with red styling", async ({ mount }) => {
+  const messages = [
+    createMockMessage({
+      type: "error",
+      content: "This is an error message",
+    }),
+  ];
+
+  const component = await mount(MobileChat, {
+    props: {
+      messages: messages,
+    },
+  });
+
+  // Check that error message is visible
+  await expect(component.locator(".message.error")).toBeVisible();
+  await expect(
+    component.locator(".message.error .message-bubble"),
+  ).toContainText("This is an error message");
+
+  // Check that error message has red styling
+  const errorBubble = component.locator(".message.error .message-bubble");
+  await expect(errorBubble).toBeVisible();
+
+  // Verify the background color and text color
+  const bgColor = await errorBubble.evaluate((el) => {
+    return window.getComputedStyle(el).backgroundColor;
+  });
+  const textColor = await errorBubble.evaluate((el) => {
+    return window.getComputedStyle(el).color;
+  });
+
+  // Check that we have red-ish colors (these will be RGB values)
+  expect(bgColor).toMatch(/rgb\(255, 235, 238\)/); // #ffebee
+  expect(textColor).toMatch(/rgb\(211, 47, 47\)/); // #d32f2f
+});
+
+test("filters messages correctly", async ({ mount }) => {
+  const messages = [
+    createMockMessage({
+      type: "user",
+      content: "User message",
+    }),
+    createMockMessage({
+      type: "agent",
+      content: "Agent message",
+    }),
+    createMockMessage({
+      type: "error",
+      content: "Error message",
+    }),
+    createMockMessage({
+      type: "tool",
+      content: "", // Empty content should be filtered out
+    }),
+    createMockMessage({
+      type: "agent",
+      content: "   ", // Whitespace-only content should be filtered out
+    }),
+  ];
+
+  const component = await mount(MobileChat, {
+    props: {
+      messages: messages,
+    },
+  });
+
+  // Should show user, agent, and error messages with content
+  await expect(component.locator(".message.user")).toBeVisible();
+  await expect(component.locator(".message.assistant")).toHaveCount(1); // Only one agent message with content
+  await expect(component.locator(".message.error")).toBeVisible();
+});
+
+test("shows thinking indicator", async ({ mount }) => {
+  const component = await mount(MobileChat, {
+    props: {
+      messages: [],
+      isThinking: true,
+    },
+  });
+
+  await expect(component.locator(".thinking-message")).toBeVisible();
+  await expect(component.locator(".thinking-text")).toContainText(
+    "Sketch is thinking",
+  );
+  await expect(component.locator(".thinking-dots")).toBeVisible();
+});
+
+test("shows empty state when no messages", async ({ mount }) => {
+  const component = await mount(MobileChat, {
+    props: {
+      messages: [],
+      isThinking: false,
+    },
+  });
+
+  await expect(component.locator(".empty-state")).toBeVisible();
+  await expect(component.locator(".empty-state")).toContainText(
+    "Start a conversation with Sketch...",
+  );
+});
+
+test("renders markdown content in assistant messages", async ({ mount }) => {
+  const messages = [
+    createMockMessage({
+      type: "agent",
+      content: "# Heading\n\n- List item 1\n- List item 2\n\n`code block`",
+    }),
+  ];
+
+  const component = await mount(MobileChat, {
+    props: {
+      messages: messages,
+    },
+  });
+
+  await expect(component.locator(".markdown-content")).toBeVisible();
+
+  // Check that markdown is rendered as HTML
+  const html = await component
+    .locator(".markdown-content")
+    .evaluate((element) => element.innerHTML);
+  expect(html).toContain("<h1>");
+  expect(html).toContain("<ul>");
+  expect(html).toContain("<code>");
+});