diff --git a/app/api/projects/[projectId]/agent-chat/route.ts b/app/api/projects/[projectId]/agent-chat/route.ts
new file mode 100644
index 0000000..39e5e95
--- /dev/null
+++ b/app/api/projects/[projectId]/agent-chat/route.ts
@@ -0,0 +1,111 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/lib/auth";
+import { query } from "@/lib/db-postgres";
+
+const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
+
+export async function POST(
+ req: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { projectId } = await params;
+ const { message } = await req.json();
+
+ if (!message?.trim()) {
+ return NextResponse.json({ error: '"message" is required' }, { status: 400 });
+ }
+
+ // Load project context to inject into the orchestrator session
+ let projectContext = "";
+ try {
+ const rows = await query<{ data: any }>(
+ `SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
+ [projectId]
+ );
+ if (rows.length > 0) {
+ const p = rows[0].data;
+ const lines = [
+ `Project: ${p.productName ?? p.name ?? "Unnamed"}`,
+ p.productVision ? `Vision: ${p.productVision}` : null,
+ p.giteaRepo ? `Gitea repo: ${p.giteaRepo}` : null,
+ p.coolifyAppUuid ? `Coolify app UUID: ${p.coolifyAppUuid}` : null,
+ p.deploymentUrl ? `Live URL: ${p.deploymentUrl}` : null,
+ p.theiaWorkspaceUrl ? `IDE: ${p.theiaWorkspaceUrl}` : null,
+ ].filter(Boolean);
+ projectContext = lines.join("\n");
+ }
+ } catch {
+ // Non-fatal — orchestrator still works without extra context
+ }
+
+ // Use projectId as the session ID so each project has its own conversation
+ const sessionId = `project_${projectId}`;
+
+ // First message in a new session? Prepend project context
+ const enrichedMessage = projectContext
+ ? `[Project context]\n${projectContext}\n\n[User message]\n${message}`
+ : message;
+
+ try {
+ const res = await fetch(`${AGENT_RUNNER_URL}/orchestrator/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: enrichedMessage, session_id: sessionId }),
+ signal: AbortSignal.timeout(120_000), // 2 min — agents can take time
+ });
+
+ if (!res.ok) {
+ const errText = await res.text();
+ return NextResponse.json(
+ { error: `Agent runner error: ${res.status} — ${errText.slice(0, 200)}` },
+ { status: 502 }
+ );
+ }
+
+ const data = await res.json();
+ return NextResponse.json({
+ reply: data.reply,
+ reasoning: data.reasoning ?? null,
+ toolCalls: data.toolCalls ?? [],
+ turns: data.turns ?? 0,
+ model: data.model ?? "unknown",
+ sessionId,
+ });
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return NextResponse.json(
+ { error: msg.includes("fetch") ? "Agent runner is offline" : msg },
+ { status: 503 }
+ );
+ }
+}
+
+// Clear session for this project
+export async function DELETE(
+ _req: NextRequest,
+ { params }: { params: Promise<{ projectId: string }> }
+) {
+ const session = await getServerSession(authOptions);
+ if (!session?.user?.email) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { projectId } = await params;
+ const sessionId = `project_${projectId}`;
+
+ try {
+ await fetch(`${AGENT_RUNNER_URL}/orchestrator/sessions/${sessionId}`, {
+ method: "DELETE",
+ });
+ } catch {
+ // Best-effort
+ }
+
+ return NextResponse.json({ cleared: true });
+}
diff --git a/components/OrchestratorChat.tsx b/components/OrchestratorChat.tsx
new file mode 100644
index 0000000..46cb9d3
--- /dev/null
+++ b/components/OrchestratorChat.tsx
@@ -0,0 +1,339 @@
+"use client";
+
+import { useState, useRef, useEffect, useCallback } from "react";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Send,
+ Loader2,
+ Wrench,
+ Bot,
+ User,
+ RotateCcw,
+ ChevronDown,
+ ChevronUp,
+ Sparkles,
+} from "lucide-react";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface Message {
+ role: "user" | "assistant";
+ content: string;
+ toolCalls?: string[];
+ turns?: number;
+ model?: string;
+ reasoning?: string | null;
+ error?: boolean;
+}
+
+interface OrchestratorChatProps {
+ projectId: string;
+ projectName?: string;
+ placeholder?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Suggestion chips shown before the first message
+// ---------------------------------------------------------------------------
+
+const SUGGESTIONS = [
+ "What's the current status of this project?",
+ "Check if there are any open issues or PRs",
+ "What was the last deployment?",
+ "Write a quick summary of what's been built so far",
+];
+
+// ---------------------------------------------------------------------------
+// Single message bubble
+// ---------------------------------------------------------------------------
+
+function MessageBubble({ msg }: { msg: Message }) {
+ const [showReasoning, setShowReasoning] = useState(false);
+ const isUser = msg.role === "user";
+
+ return (
+
+ {/* Avatar */}
+
+ {isUser ? : }
+
+
+ {/* Bubble */}
+
+
+ {msg.content}
+
+
+ {/* Tool calls & meta */}
+ {!isUser && (
+
+ {msg.toolCalls && msg.toolCalls.length > 0 && (
+
+ {msg.toolCalls.map((t, i) => (
+
+
+ {t}
+
+ ))}
+
+ )}
+ {msg.reasoning && (
+
+ )}
+ {msg.model && (
+
{msg.model}
+ )}
+
+ )}
+
+ {/* Reasoning panel */}
+ {!isUser && showReasoning && msg.reasoning && (
+
+ {msg.reasoning}
+
+ )}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Typing indicator
+// ---------------------------------------------------------------------------
+
+function TypingIndicator() {
+ return (
+
+
+
+
+
+
+ {[0, 1, 2].map(i => (
+
+ ))}
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+
+export function OrchestratorChat({
+ projectId,
+ projectName,
+ placeholder = "Ask your AI team anything…",
+}: OrchestratorChatProps) {
+ const [messages, setMessages] = useState
([]);
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const bottomRef = useRef(null);
+ const textareaRef = useRef(null);
+ const hasMessages = messages.length > 0;
+
+ const scrollToBottom = useCallback(() => {
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, []);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [messages, loading]);
+
+ const sendMessage = useCallback(
+ async (text: string) => {
+ const trimmed = text.trim();
+ if (!trimmed || loading) return;
+
+ setInput("");
+ setMessages(prev => [...prev, { role: "user", content: trimmed }]);
+ setLoading(true);
+
+ try {
+ const res = await fetch(`/api/projects/${projectId}/agent-chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: trimmed }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ setMessages(prev => [
+ ...prev,
+ { role: "assistant", content: data.error ?? "Something went wrong.", error: true },
+ ]);
+ } else {
+ setMessages(prev => [
+ ...prev,
+ {
+ role: "assistant",
+ content: data.reply || "(no reply)",
+ toolCalls: data.toolCalls,
+ turns: data.turns,
+ model: data.model,
+ reasoning: data.reasoning,
+ },
+ ]);
+ }
+ } catch (err) {
+ setMessages(prev => [
+ ...prev,
+ {
+ role: "assistant",
+ content: err instanceof Error ? err.message : "Network error — is the agent runner online?",
+ error: true,
+ },
+ ]);
+ } finally {
+ setLoading(false);
+ setTimeout(() => textareaRef.current?.focus(), 50);
+ }
+ },
+ [projectId, loading]
+ );
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage(input);
+ }
+ };
+
+ const clearChat = async () => {
+ setMessages([]);
+ try {
+ await fetch(`/api/projects/${projectId}/agent-chat`, { method: "DELETE" });
+ } catch { /* best-effort */ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ {projectName ? `${projectName} AI` : "Project AI"}
+
+
GLM-5
+
+ {hasMessages && (
+
+ )}
+
+
+ {/* Messages */}
+
+ {!hasMessages ? (
+ /* Empty state — Lovable-style centered prompt */
+
+
+
What should we build?
+
+ Your AI team is ready. Ask them anything about this project.
+
+
+
+ {SUGGESTIONS.map(s => (
+
+ ))}
+
+
+ ) : (
+
+
+ {messages.map((msg, i) => (
+
+ ))}
+ {loading &&
}
+
+
+
+ )}
+
+
+ {/* Input */}
+
+
+
+
+ Enter to send · Shift+Enter for newline
+
+
+
+ );
+}