diff --git a/app/[workspace]/project/[projectId]/overview/page.tsx b/app/[workspace]/project/[projectId]/overview/page.tsx
index 550968d..ff0bf00 100644
--- a/app/[workspace]/project/[projectId]/overview/page.tsx
+++ b/app/[workspace]/project/[projectId]/overview/page.tsx
@@ -13,6 +13,7 @@ import {
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { OrchestratorChat } from "@/components/OrchestratorChat";
+import { AtlasChat } from "@/components/AtlasChat";
import {
GitBranch,
GitCommit,
@@ -78,6 +79,9 @@ interface Project {
deploymentUrl?: string;
// Theia
theiaWorkspaceUrl?: string;
+ // Stage
+ stage?: 'discovery' | 'architecture' | 'building' | 'active';
+ prd?: string;
// Context
contextSnapshot?: ContextSnapshot;
stats?: { sessions: number; costs: number };
@@ -199,11 +203,18 @@ export default function ProjectOverviewPage() {
return (
- {/* ── Orchestrator Chat ── */}
-
+ {/* ── Agent Panel — Atlas for discovery, Orchestrator once PRD is done ── */}
+ {(!project.stage || project.stage === 'discovery') ? (
+
+ ) : (
+
+ )}
{/* ── Header ── */}
diff --git a/app/api/projects/[projectId]/atlas-chat/route.ts b/app/api/projects/[projectId]/atlas-chat/route.ts
new file mode 100644
index 0000000..8216315
--- /dev/null
+++ b/app/api/projects/[projectId]/atlas-chat/route.ts
@@ -0,0 +1,164 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/lib/auth/authOptions";
+import { query } from "@/lib/db-postgres";
+
+const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333";
+
+// ---------------------------------------------------------------------------
+// DB helpers — atlas_conversations table
+// ---------------------------------------------------------------------------
+
+let tableReady = false;
+
+async function ensureTable() {
+ if (tableReady) return;
+ await query(`
+ CREATE TABLE IF NOT EXISTS atlas_conversations (
+ project_id TEXT PRIMARY KEY,
+ messages JSONB NOT NULL DEFAULT '[]'::jsonb,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+ )
+ `);
+ tableReady = true;
+}
+
+async function loadAtlasHistory(projectId: string): Promise
{
+ try {
+ await ensureTable();
+ const rows = await query<{ messages: any[] }>(
+ `SELECT messages FROM atlas_conversations WHERE project_id = $1`,
+ [projectId]
+ );
+ return rows[0]?.messages ?? [];
+ } catch {
+ return [];
+ }
+}
+
+async function saveAtlasHistory(projectId: string, messages: any[]): Promise {
+ try {
+ await ensureTable();
+ await query(
+ `INSERT INTO atlas_conversations (project_id, messages, updated_at)
+ VALUES ($1, $2::jsonb, NOW())
+ ON CONFLICT (project_id) DO UPDATE
+ SET messages = $2::jsonb, updated_at = NOW()`,
+ [projectId, JSON.stringify(messages)]
+ );
+ } catch (e) {
+ console.error("[atlas-chat] Failed to save history:", e);
+ }
+}
+
+async function savePrd(projectId: string, prdContent: string): Promise {
+ try {
+ await query(
+ `UPDATE fs_projects
+ SET data = data || jsonb_build_object('prd', $2, 'stage', 'architecture'),
+ updated_at = NOW()
+ WHERE id = $1`,
+ [projectId, prdContent]
+ );
+ console.log(`[atlas-chat] PRD saved for project ${projectId}`);
+ } catch (e) {
+ console.error("[atlas-chat] Failed to save PRD:", e);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// POST — send message to Atlas
+// ---------------------------------------------------------------------------
+
+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 });
+ }
+
+ const sessionId = `atlas_${projectId}`;
+
+ // Load conversation history from DB to persist across agent runner restarts
+ const history = await loadAtlasHistory(projectId);
+
+ try {
+ const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ message,
+ session_id: sessionId,
+ history,
+ }),
+ signal: AbortSignal.timeout(120_000),
+ });
+
+ if (!res.ok) {
+ const text = await res.text();
+ console.error("[atlas-chat] Agent runner error:", text);
+ return NextResponse.json(
+ { error: "Atlas is unavailable. Please try again." },
+ { status: 502 }
+ );
+ }
+
+ const data = await res.json();
+
+ // Persist updated history
+ await saveAtlasHistory(projectId, data.history ?? []);
+
+ // If Atlas finalized the PRD, save it to the project
+ if (data.prdContent) {
+ await savePrd(projectId, data.prdContent);
+ }
+
+ return NextResponse.json({
+ reply: data.reply,
+ sessionId,
+ prdContent: data.prdContent ?? null,
+ model: data.model ?? null,
+ });
+ } catch (err) {
+ console.error("[atlas-chat] Error:", err);
+ return NextResponse.json(
+ { error: "Request timed out or failed. Please try again." },
+ { status: 500 }
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// DELETE — clear Atlas conversation 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 = `atlas_${projectId}`;
+
+ try {
+ await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${sessionId}`, { method: "DELETE" });
+ } catch { /* runner may be down */ }
+
+ try {
+ await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]);
+ } catch { /* table may not exist yet */ }
+
+ return NextResponse.json({ cleared: true });
+}
diff --git a/components/AtlasChat.tsx b/components/AtlasChat.tsx
new file mode 100644
index 0000000..3550387
--- /dev/null
+++ b/components/AtlasChat.tsx
@@ -0,0 +1,315 @@
+"use client";
+
+import { useState, useRef, useEffect, useCallback } from "react";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import {
+ Send,
+ Loader2,
+ User,
+ RotateCcw,
+ FileText,
+ CheckCircle2,
+ ChevronDown,
+ ChevronUp,
+} from "lucide-react";
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface Message {
+ role: "user" | "assistant";
+ content: string;
+}
+
+interface AtlasChatProps {
+ projectId: string;
+ projectName?: string;
+ initialMessage?: string;
+}
+
+// ---------------------------------------------------------------------------
+// Atlas avatar — distinct from orchestrator
+// ---------------------------------------------------------------------------
+
+function AtlasAvatar() {
+ return (
+
+ A
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// PRD preview panel
+// ---------------------------------------------------------------------------
+
+function PrdPanel({ content }: { content: string }) {
+ const [expanded, setExpanded] = useState(false);
+ const preview = content.split("\n").slice(0, 6).join("\n");
+
+ return (
+
+
+
+
+ PRD Generated
+
+
+ Ready for architecture phase
+
+
+
+
+
+ {expanded ? content : preview}
+
+ {!expanded && (
+
+ )}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main component
+// ---------------------------------------------------------------------------
+
+export function AtlasChat({ projectId, projectName, initialMessage }: AtlasChatProps) {
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [prdContent, setPrdContent] = useState(null);
+ const [error, setError] = useState(null);
+ const [started, setStarted] = useState(false);
+ const bottomRef = useRef(null);
+ const textareaRef = useRef(null);
+
+ // Auto-scroll to latest message
+ useEffect(() => {
+ bottomRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages, loading]);
+
+ // Kick off Atlas with its opening message on first load
+ useEffect(() => {
+ if (started) return;
+ setStarted(true);
+ sendMessage(
+ initialMessage ||
+ `Hey — I'm starting a new project called "${projectName || "my project"}". I'd love your help defining what we're building.`
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const sendMessage = useCallback(
+ async (text: string) => {
+ if (!text.trim() || loading) return;
+ setError(null);
+
+ const userMsg: Message = { role: "user", content: text };
+ setMessages((prev) => [...prev, userMsg]);
+ setInput("");
+ setLoading(true);
+
+ try {
+ const res = await fetch(
+ `/api/projects/${projectId}/atlas-chat`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ message: text }),
+ }
+ );
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ setError(data.error || "Something went wrong. Please try again.");
+ setMessages((prev) => prev.slice(0, -1));
+ return;
+ }
+
+ const assistantMsg: Message = {
+ role: "assistant",
+ content: data.reply || "...",
+ };
+ setMessages((prev) => [...prev, assistantMsg]);
+
+ if (data.prdContent) {
+ setPrdContent(data.prdContent);
+ }
+ } catch {
+ setError("Couldn't reach Atlas. Check your connection and try again.");
+ setMessages((prev) => prev.slice(0, -1));
+ } finally {
+ setLoading(false);
+ setTimeout(() => textareaRef.current?.focus(), 50);
+ }
+ },
+ [loading, projectId]
+ );
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage(input);
+ }
+ };
+
+ const handleReset = async () => {
+ if (!confirm("Start the discovery conversation over from scratch?")) return;
+ await fetch(`/api/projects/${projectId}/atlas-chat`, { method: "DELETE" });
+ setMessages([]);
+ setPrdContent(null);
+ setStarted(false);
+ setError(null);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Atlas
+
Product Requirements
+
+
+
+ {prdContent && (
+
+
+ PRD ready
+
+ )}
+
+
+
+
+ {/* Messages */}
+
+
+ {messages.map((msg, i) => (
+
+ {msg.role === "assistant" &&
}
+
+
+
+ {msg.role === "user" && (
+
+
+
+ )}
+
+ ))}
+
+ {/* Typing indicator */}
+ {loading && (
+
+ )}
+
+ {/* Error */}
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* PRD panel */}
+ {prdContent &&
}
+
+
+
+
+
+ {/* Input */}
+
+ {prdContent ? (
+
+ PRD complete — the platform is now architecting your solution.
+
+ ) : (
+
+
+ )}
+
+
+ );
+}