diff --git a/web/src/App.tsx b/web/src/App.tsx
index 7600d224..8c38490d 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -12,6 +12,7 @@ import { AuthGuard } from './components/auth';
import { ErrorBoundary } from './components/shared/ErrorBoundary';
import { SkipToContent } from './components/shared/SkipToContent';
import { LiveAnnouncerProvider } from './components/shared/LiveAnnouncer';
+import { FloatingChat } from './components/chat/FloatingChat';
// Main app content (only rendered when authenticated)
function AppContent() {
@@ -38,6 +39,7 @@ function AppContent() {
+
diff --git a/web/src/components/chat/FloatingChat.tsx b/web/src/components/chat/FloatingChat.tsx
new file mode 100644
index 00000000..a2bbc1ab
--- /dev/null
+++ b/web/src/components/chat/FloatingChat.tsx
@@ -0,0 +1,69 @@
+import { useState, useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import { MessageSquare } from 'lucide-react';
+import { ChatPanel } from './ChatPanel';
+import { chatEventTarget } from '@/hooks/useTaskSync';
+import { cn } from '@/lib/utils';
+
+/**
+ * Floating chat bubble — bottom-right corner.
+ * Opens a board-level ChatPanel (no taskId).
+ * Pulses when a new response arrives while closed.
+ */
+export function FloatingChat() {
+ const [open, setOpen] = useState(false);
+ const [hasUnread, setHasUnread] = useState(false);
+
+ // Listen for incoming chat messages when panel is closed
+ useEffect(() => {
+ const handler = (e: Event) => {
+ const msg = (e as CustomEvent).detail;
+ // Only pulse for board-level chat messages (no taskId in sessionId)
+ if (
+ !open &&
+ (msg.type === 'chat:message' || msg.type === 'chat:delta') &&
+ msg.sessionId &&
+ !msg.sessionId.includes('task_')
+ ) {
+ setHasUnread(true);
+ }
+ };
+
+ chatEventTarget.addEventListener('chat', handler);
+ return () => chatEventTarget.removeEventListener('chat', handler);
+ }, [open]);
+
+ // Clear unread when opening
+ const handleOpen = () => {
+ setOpen(true);
+ setHasUnread(false);
+ };
+
+ return (
+ <>
+ {/* Floating button */}
+
+
+ {/* Chat panel — board-level (no taskId) */}
+
+ >
+ );
+}