diff --git a/bff/app.py b/bff/app.py
index 50a5dbf..f454ffe 100644
--- a/bff/app.py
+++ b/bff/app.py
@@ -138,9 +138,33 @@ async def list_agents(user: dict = Depends(get_current_user)):
return await tracker_get("/api/v1/agents/")
-@app.get("/api/v1/agents/adapters")
-async def list_adapters(user: dict = Depends(get_current_user)):
- return await tracker_get("/api/v1/agents/adapters")
+# --- Proxy: Chats ---
+
+@app.get("/api/v1/chats/lobby")
+async def get_lobby(user: dict = Depends(get_current_user)):
+ return await tracker_get("/api/v1/chats/lobby")
+
+
+@app.get("/api/v1/chats/project/{project_id}")
+async def get_project_chat(project_id: str, user: dict = Depends(get_current_user)):
+ return await tracker_get(f"/api/v1/chats/project/{project_id}")
+
+
+@app.get("/api/v1/chats/{chat_id}/messages")
+async def list_messages(chat_id: str, limit: int = 50, before: str = None, user: dict = Depends(get_current_user)):
+ params = {"limit": limit}
+ if before:
+ params["before"] = before
+ return await tracker_get(f"/api/v1/chats/{chat_id}/messages", params=params)
+
+
+@app.post("/api/v1/chats/{chat_id}/messages")
+async def create_message(chat_id: str, request: Request, user: dict = Depends(get_current_user)):
+ body = await request.json()
+ # Inject sender info from auth
+ body.setdefault("sender_type", "human")
+ body.setdefault("sender_name", user.get("name", "human"))
+ return await tracker_post(f"/api/v1/chats/{chat_id}/messages", json=body)
# --- Proxy: Labels ---
diff --git a/src/app/(protected)/projects/[slug]/page.tsx b/src/app/(protected)/projects/[slug]/page.tsx
index 99fe1f6..b3185a0 100644
--- a/src/app/(protected)/projects/[slug]/page.tsx
+++ b/src/app/(protected)/projects/[slug]/page.tsx
@@ -5,6 +5,7 @@ import { useParams } from "next/navigation";
import { getProjects, Project } from "@/lib/api";
import Sidebar from "@/components/Sidebar";
import KanbanBoard from "@/components/KanbanBoard";
+import ChatPanel from "@/components/ChatPanel";
export default function ProjectPage() {
const { slug } = useParams<{ slug: string }>();
@@ -40,6 +41,7 @@ export default function ProjectPage() {
)}
+
diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx
new file mode 100644
index 0000000..cef9bdb
--- /dev/null
+++ b/src/components/ChatPanel.tsx
@@ -0,0 +1,195 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import {
+ Chat,
+ ChatMessage,
+ getProjectChat,
+ getChatMessages,
+ sendChatMessage,
+} from "@/lib/api";
+import { wsClient } from "@/lib/ws";
+
+interface ChatPanelProps {
+ projectId: string;
+}
+
+export default function ChatPanel({ projectId }: ChatPanelProps) {
+ const [chat, setChat] = useState(null);
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState("");
+ const [expanded, setExpanded] = useState(false);
+ const [collapsed, setCollapsed] = useState(false);
+ const [sending, setSending] = useState(false);
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Load chat and messages
+ useEffect(() => {
+ let cancelled = false;
+ getProjectChat(projectId).then((c) => {
+ if (cancelled) return;
+ setChat(c);
+ getChatMessages(c.id).then((msgs) => {
+ if (!cancelled) setMessages(msgs);
+ });
+ });
+ return () => { cancelled = true; };
+ }, [projectId]);
+
+ // WebSocket: subscribe to chat room
+ useEffect(() => {
+ if (!chat) return;
+
+ wsClient.connect();
+ wsClient.subscribeChat(chat.id);
+
+ const unsub = wsClient.on("chat.message", (msg: any) => {
+ if (msg.chat_id === chat.id) {
+ setMessages((prev) => [
+ ...prev,
+ {
+ id: msg.id,
+ chat_id: msg.chat_id,
+ sender_type: msg.sender_type,
+ sender_id: msg.sender_id,
+ sender_name: msg.sender_name,
+ content: msg.content,
+ created_at: msg.created_at,
+ },
+ ]);
+ }
+ });
+
+ return () => {
+ wsClient.unsubscribeChat(chat.id);
+ unsub();
+ };
+ }, [chat]);
+
+ // Auto-scroll
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ const handleSend = async () => {
+ if (!chat || !input.trim() || sending) return;
+ const text = input.trim();
+ setInput("");
+ setSending(true);
+
+ try {
+ // Send via REST (gets saved + we get it back via WS broadcast)
+ const msg = await sendChatMessage(chat.id, text);
+ // Add immediately (WS might duplicate — deduplicate by id)
+ setMessages((prev) => {
+ if (prev.find((m) => m.id === msg.id)) return prev;
+ return [...prev, msg];
+ });
+ } catch (e) {
+ console.error("Failed to send message:", e);
+ setInput(text); // restore on failure
+ } finally {
+ setSending(false);
+ inputRef.current?.focus();
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ if (collapsed) {
+ return (
+
+
+
+ );
+ }
+
+ const panelHeight = expanded ? "h-full" : "h-[30vh]";
+
+ return (
+
+ {/* Header */}
+
+
💬 Чат проекта
+
+
+
+
+
+
+ {/* Messages */}
+
+ {messages.length === 0 && (
+
+ Пока пусто. Напишите первое сообщение 💬
+
+ )}
+ {messages.map((msg) => (
+
+
+ {msg.sender_name || msg.sender_type}:
+
+ {msg.content}
+
+ {new Date(msg.created_at).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" })}
+
+
+ ))}
+
+
+
+ {/* Input */}
+
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder="Написать сообщение..."
+ className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded px-3 py-1.5 text-sm outline-none focus:border-[var(--accent)]"
+ disabled={!chat || sending}
+ />
+
+
+
+ );
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 583cb75..cc609c8 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -61,7 +61,7 @@ export interface Agent {
id: string;
name: string;
slug: string;
- adapter_id: string;
+ capabilities: string[];
subscription_mode: string;
max_concurrent: number;
status: string;
@@ -93,5 +93,34 @@ export const deleteTask = (id: string) =>
// Agents
export const getAgents = () => request("/api/v1/agents/");
+// Chats
+export interface Chat {
+ id: string;
+ project_id: string | null;
+ task_id: string | null;
+ kind: string;
+}
+
+export interface ChatMessage {
+ id: string;
+ chat_id: string;
+ sender_type: string;
+ sender_id: string | null;
+ sender_name: string | null;
+ content: string;
+ created_at: string;
+}
+
+export const getLobbyChat = () => request("/api/v1/chats/lobby");
+export const getProjectChat = (projectId: string) =>
+ request(`/api/v1/chats/project/${projectId}`);
+export const getChatMessages = (chatId: string, limit = 50) =>
+ request(`/api/v1/chats/${chatId}/messages?limit=${limit}`);
+export const sendChatMessage = (chatId: string, content: string) =>
+ request(`/api/v1/chats/${chatId}/messages`, {
+ method: "POST",
+ body: JSON.stringify({ content }),
+ });
+
// Labels
export const getLabels = () => request