From 99c1a83b9fc4ee6034b564d1762c7a59bbfca969 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 10 Mar 2026 16:28:44 -0700 Subject: [PATCH] feat: load Atlas discovery history into CooChat sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the two-chat experience on the overview page: - CooChat now pre-loads Atlas conversation history on mount, showing the full discovery conversation in the left panel. Atlas messages render with a blue "A" avatar; COO messages use the dark "◈" icon. A "Discovery · COO" divider separates historical from new messages. - FreshIdeaMain detects when a PRD already exists and replaces the duplicate AtlasChat with a clean completion view ("Discovery complete") that links to the PRD and Build pages. Atlas chat only shows when discovery is still in progress. Made-with: Cursor --- components/layout/coo-chat.tsx | 212 ++++++++++++++++------ components/project-main/FreshIdeaMain.tsx | 64 +++++++ 2 files changed, 218 insertions(+), 58 deletions(-) diff --git a/components/layout/coo-chat.tsx b/components/layout/coo-chat.tsx index 9281478..18465e4 100644 --- a/components/layout/coo-chat.tsx +++ b/components/layout/coo-chat.tsx @@ -6,40 +6,86 @@ interface CooMessage { id: string; role: "user" | "assistant"; content: string; + source?: "atlas" | "coo"; // atlas = discovery history, coo = orchestrator response streaming?: boolean; } -const WELCOME: CooMessage = { - id: "welcome", - role: "assistant", - content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?", -}; - export function CooChat({ projectId }: { projectId: string }) { - const [messages, setMessages] = useState([WELCOME]); + const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); + const [historyLoaded, setHistoryLoaded] = useState(false); const bottomRef = useRef(null); const textareaRef = useRef(null); + // Scroll to bottom whenever messages change useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); + // Pre-load Atlas discovery history on mount + useEffect(() => { + fetch(`/api/projects/${projectId}/atlas-chat`) + .then(r => r.json()) + .then((data: { messages?: Array<{ role: "user" | "assistant"; content: string }> }) => { + const atlasMessages: CooMessage[] = (data.messages ?? []) + .filter(m => m.content?.trim()) + .map((m, i) => ({ + id: `atlas_${i}`, + role: m.role, + content: m.content, + source: "atlas" as const, + })); + + if (atlasMessages.length > 0) { + // Add a small divider message at the bottom of Atlas history + setMessages([ + ...atlasMessages, + { + id: "coo_divider", + role: "assistant", + content: "Discovery complete. I'm your product COO — I have the full context above. What do you need?", + source: "coo" as const, + }, + ]); + } else { + // No Atlas history — show default COO welcome + setMessages([{ + id: "welcome", + role: "assistant", + content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?", + source: "coo" as const, + }]); + } + setHistoryLoaded(true); + }) + .catch(() => { + setMessages([{ + id: "welcome", + role: "assistant", + content: "Hi. I'm your product COO — I know your codebase, your goals, and what's been built. What do you need?", + source: "coo" as const, + }]); + setHistoryLoaded(true); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectId]); + const send = async () => { const text = input.trim(); if (!text || loading) return; setInput(""); - const userMsg: CooMessage = { id: Date.now().toString(), role: "user", content: text }; + const userMsg: CooMessage = { id: Date.now().toString(), role: "user", content: text, source: "coo" }; const assistantId = (Date.now() + 1).toString(); - const assistantMsg: CooMessage = { id: assistantId, role: "assistant", content: "", streaming: true }; + const assistantMsg: CooMessage = { id: assistantId, role: "assistant", content: "", source: "coo", streaming: true }; setMessages(prev => [...prev, userMsg, assistantMsg]); setLoading(true); + // Build history from COO messages only (skip atlas history for context to orchestrator) const history = messages - .filter(m => m.id !== "welcome" && m.content) + .filter(m => m.source === "coo" && m.id !== "coo_divider" && m.content) .map(m => ({ role: m.role === "assistant" ? "model" as const : "user" as const, content: m.content })); try { @@ -79,59 +125,109 @@ export function CooChat({ projectId }: { projectId: string }) { } }; + if (!historyLoaded) { + return ( +
+
+ {[0, 1, 2].map(i => ( + + ))} +
+
+ ); + } + return (
{/* Messages */} -
- {messages.map(msg => ( -
- {msg.role === "assistant" && ( - - )} -
- {msg.content} - {msg.streaming && msg.content === "" && ( - - {[0, 1, 2].map(i => ( - + {messages.map((msg, idx) => { + const isAtlas = msg.source === "atlas"; + const isUser = msg.role === "user"; + const isCoo = !isUser && !isAtlas; + + // Separator before the divider message + const prevMsg = messages[idx - 1]; + const showSeparator = msg.id === "coo_divider" && prevMsg?.source === "atlas"; + + return ( +
+ {showSeparator && ( +
+
+ + Discovery · COO + +
+
+ )} + +
+ {/* Avatar */} + {!isUser && ( + + {isAtlas ? "A" : "◈"} + + )} + +
+ {msg.content} + {msg.streaming && msg.content === "" && ( + + {[0, 1, 2].map(i => ( + + ))} + + )} + {msg.streaming && msg.content !== "" && ( + - ))} - - )} - {msg.streaming && msg.content !== "" && ( - - )} + )} +
+
-
- ))} + ); + })}
diff --git a/components/project-main/FreshIdeaMain.tsx b/components/project-main/FreshIdeaMain.tsx index cb7dabb..2628ae4 100644 --- a/components/project-main/FreshIdeaMain.tsx +++ b/components/project-main/FreshIdeaMain.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { AtlasChat } from "@/components/AtlasChat"; import { useRouter, useParams } from "next/navigation"; +import Link from "next/link"; const DISCOVERY_PHASES = [ "big_picture", @@ -27,8 +28,15 @@ export function FreshIdeaMain({ projectId, projectName }: FreshIdeaMainProps) { const [allDone, setAllDone] = useState(false); const [prdLoading, setPrdLoading] = useState(false); const [dismissed, setDismissed] = useState(false); + const [hasPrd, setHasPrd] = useState(false); useEffect(() => { + // Check if PRD already exists on the project + fetch(`/api/projects/${projectId}`) + .then(r => r.json()) + .then(d => { if (d.project?.prd) setHasPrd(true); }) + .catch(() => {}); + const poll = () => { fetch(`/api/projects/${projectId}/save-phase`) .then(r => r.json()) @@ -59,6 +67,62 @@ export function FreshIdeaMain({ projectId, projectName }: FreshIdeaMainProps) { router.push(`/${workspace}/project/${projectId}/build`); }; + // Once the PRD exists, show a clean "done" state in the main panel. + // The Atlas conversation history lives in the left CooChat sidebar. + if (hasPrd) { + return ( +
+
+
+
+
+ Discovery complete +
+
+ Your PRD is saved. The full discovery conversation is in the left panel — talk to your COO to plan what to build next. +
+
+
+ + View PRD → + + + Go to Build + +
+
+
+ ); + } + return (
{/* Decision banner — shown when all 6 phases are saved */}