webui: Fix demo page

- Change asset root for vite
- Update mock handlers to support SSE by replacing long polling implementation with Server-Sent Events (SSE) in webui mock handlers.

Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/webui/src/web-components/demo/mocks/handlers.ts b/webui/src/web-components/demo/mocks/handlers.ts
index c05a6c4..ad3426e 100644
--- a/webui/src/web-components/demo/mocks/handlers.ts
+++ b/webui/src/web-components/demo/mocks/handlers.ts
@@ -1,7 +1,8 @@
 import { http, HttpResponse, delay } from "msw";
 import { initialState, initialMessages } from "../../../fixtures/dummy";
+import { AgentMessage, State } from "../../../types";
 
-// Mock state updates for long-polling simulation
+// Mock state updates for SSE simulation
 let currentState = { ...initialState };
 const EMPTY_CONVERSATION =
   new URL(window.location.href).searchParams.get("emptyConversation") === "1";
@@ -10,49 +11,106 @@
 
 const messages = EMPTY_CONVERSATION ? [] : [...initialMessages];
 
+// Text encoder for SSE messages
+const encoder = new TextEncoder();
+
+// Helper function to create SSE formatted messages
+function formatSSE(event, data) {
+  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
+}
+
 export const handlers = [
-  // Unified state endpoint that handles both regular and polling requests
-  http.get("*/state", async ({ request }) => {
+  // SSE stream endpoint
+  http.get("*/stream", async ({ request }) => {
     const url = new URL(request.url);
-    const isPoll = url.searchParams.get("poll") === "true";
+    const fromIndex = parseInt(url.searchParams.get("from") || "0");
 
-    if (!isPoll) {
-      // Regular state request
-      return HttpResponse.json(currentState);
-    }
+    // Create a readable stream for SSE
+    const stream = new ReadableStream({
+      async start(controller) {
+        // Send initial state update
+        controller.enqueue(encoder.encode(formatSSE("state", currentState)));
 
-    // This is a long-polling request
-    await delay(ADD_NEW_MESSAGES ? 2000 : 60000); // Simulate waiting for changes
+        // Send any existing messages that are newer than the fromIndex
+        const newMessages = messages.filter((msg) => msg.idx >= fromIndex);
+        for (const message of newMessages) {
+          controller.enqueue(encoder.encode(formatSSE("message", message)));
+        }
 
-    if (ADD_NEW_MESSAGES) {
-      // Simulate adding new messages
-      messages.push({
-        type: "agent",
-        end_of_turn: false,
-        content: "Here's a message",
-        timestamp: "2025-04-24T10:32:29.072661+01:00",
-        conversation_id: "37s-g6xg",
-        usage: {
-          input_tokens: 5,
-          cache_creation_input_tokens: 250,
-          cache_read_input_tokens: 4017,
-          output_tokens: 92,
-          cost_usd: 0.0035376,
-        },
-        start_time: "2025-04-24T10:32:26.99749+01:00",
-        end_time: "2025-04-24T10:32:29.072654+01:00",
-        elapsed: 2075193375,
-        turnDuration: 28393844125,
-        idx: messages.length,
-      });
+        // Simulate heartbeats and new messages
+        let heartbeatInterval;
+        let messageInterval;
 
-      // Update the state with new messages
-      currentState = {
-        ...currentState,
-        message_count: messages.length,
-      };
-    }
+        // Send heartbeats every 30 seconds
+        heartbeatInterval = setInterval(() => {
+          controller.enqueue(
+            encoder.encode(
+              formatSSE("heartbeat", { timestamp: new Date().toISOString() }),
+            ),
+          );
+        }, 30000);
 
+        // Add new messages if enabled
+        if (ADD_NEW_MESSAGES) {
+          messageInterval = setInterval(() => {
+            const newMessage = {
+              type: "agent" as const,
+              end_of_turn: false,
+              content: "Here's a new message via SSE",
+              timestamp: new Date().toISOString(),
+              conversation_id: "37s-g6xg",
+              usage: {
+                input_tokens: 5,
+                cache_creation_input_tokens: 250,
+                cache_read_input_tokens: 4017,
+                output_tokens: 92,
+                cost_usd: 0.0035376,
+              },
+              start_time: new Date(Date.now() - 2000).toISOString(),
+              end_time: new Date().toISOString(),
+              elapsed: 2075193375,
+              turnDuration: 28393844125,
+              idx: messages.length,
+            };
+
+            // Add to our messages array
+            messages.push(newMessage);
+
+            // Update the state
+            currentState = {
+              ...currentState,
+              message_count: messages.length,
+            };
+
+            // Send the message and updated state through SSE
+            controller.enqueue(
+              encoder.encode(formatSSE("message", newMessage)),
+            );
+            controller.enqueue(
+              encoder.encode(formatSSE("state", currentState)),
+            );
+          }, 2000); // Add a new message every 2 seconds
+        }
+
+        // Clean up on connection close
+        request.signal.addEventListener("abort", () => {
+          clearInterval(heartbeatInterval);
+          if (messageInterval) clearInterval(messageInterval);
+        });
+      },
+    });
+
+    return new HttpResponse(stream, {
+      headers: {
+        "Content-Type": "text/event-stream",
+        "Cache-Control": "no-cache",
+        Connection: "keep-alive",
+      },
+    });
+  }),
+
+  // State endpoint (non-streaming version for initial state)
+  http.get("*/state", () => {
     return HttpResponse.json(currentState);
   }),
 
@@ -63,4 +121,35 @@
 
     return HttpResponse.json(messages.slice(startIndex));
   }),
+
+  // Chat endpoint for sending messages
+  http.post("*/chat", async ({ request }) => {
+    const body = await request.json();
+
+    // Add a user message
+    messages.push({
+      type: "user" as const,
+      end_of_turn: true,
+      content:
+        typeof body === "object" && body !== null
+          ? String(body.message || "")
+          : "",
+      timestamp: new Date().toISOString(),
+      conversation_id: "37s-g6xg",
+      idx: messages.length,
+    });
+
+    // Update state
+    currentState = {
+      ...currentState,
+      message_count: messages.length,
+    };
+
+    return new HttpResponse(null, { status: 204 });
+  }),
+
+  // Cancel endpoint
+  http.post("*/cancel", () => {
+    return new HttpResponse(null, { status: 204 });
+  }),
 ];
diff --git a/webui/vite.config.mts b/webui/vite.config.mts
index 2d99b63..9765931 100644
--- a/webui/vite.config.mts
+++ b/webui/vite.config.mts
@@ -33,7 +33,7 @@
     middlewareMode: false,
     fs: {
       // Allow serving files from these directories
-      allow: ["/app/webui"],
+      allow: ["."],
     },
   },
 });