import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "react-router"; import { FileTree, type FileNode } from "#/components/ide/FileTree"; import { Button } from "#/components/ui/Button"; import { ConversationApi } from "#/api/conversation-service/conversation-service.api"; import { useArtifacts } from "#/components/layout/ArtifactsContext"; import { useMode } from "#/components/layout/ModeContext"; import { suggestMode, type ModeId } from "#/types/mode"; import { AgentTimeline } from "#/components/ui/AgentTimeline"; import { PlanView } from "#/components/ui/PlanView"; import { IDEWorkspace } from "#/components/ide/IDEWorkspace"; import { StackTraceViewer } from "#/components/ui/StackTraceViewer"; import type { FixSuggestion, ReviewFile, Message, TimelineEvent, PlanStep, DebugError, TerminalLine, } from "#/types/conversation"; import { ReviewWorkspace } from "#/components/ui/ReviewWorkspace"; import { AutonomousOrchestrator } from "#/components/ui/AutonomousOrchestrator"; import { MessageBubble } from "#/components/conversation/MessageBubble"; import { ErrorBoundary } from "#/components/ErrorBoundary"; import { ThinkingPanel, type ThinkingStep } from "#/components/ui/ThinkingPanel"; import { Terminal } from "#/components/Terminal"; import { SuggestionTextarea } from "#/components/conversation/SuggestionTextarea"; import { useConversationWebSocket } from "#/hooks/use-conversation-websocket"; /* ── Page Component ── */ export default function ConversationPage() { return ( ); } function ConversationContent() { const { conversationId } = useParams<{ conversationId: string }>(); const artifacts = useArtifacts(); const { mode: contextMode } = useMode(); const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [focused, setFocused] = useState(false); const [isConnecting, setIsConnecting] = useState(true); const [isRunning, setIsRunning] = useState(false); const [error, setError] = useState(null); const [conversation, setConversation] = useState<{ sessionApiKey: string; agentServerUrl: string; sandboxStatus: string; executionStatus?: string | null; title?: string | null; } | null>(null); const [needsResume, setNeedsResume] = useState(false); const [isResuming, setIsResuming] = useState(false); /* ── Mode state ── */ const [selectedMode, setSelectedMode] = useState(contextMode); const [autoMode] = useState(true); const [autoModeFlash, setAutoModeFlash] = useState(null); const [showAgentPanel, setShowAgentPanel] = useState(false); /* ── Thinking / CoT state ── */ const [thinkingSteps, setThinkingSteps] = useState([]); const [thinkingCollapsed, setThinkingCollapsed] = useState(false); /* ── Terminal visibility ── */ const [showChatTerminal, setShowChatTerminal] = useState(false); /* ── Inline suggestion for chat input ── */ const [inputSuggestion, setInputSuggestion] = useState(null); /* Generate input suggestions from agent context */ useEffect(() => { if (!input.trim() || isRunning) { setInputSuggestion(null); return; } const lastMsg = messages[messages.length - 1]; if (!lastMsg || lastMsg.role === "user") { setInputSuggestion(null); return; } /* Suggest mode-appropriate follow-ups based on last agent message */ const content = lastMsg.content.toLowerCase(); if (content.includes("fix") || content.includes("error")) { setInputSuggestion(" and test it"); } else if (content.includes("plan")) { setInputSuggestion(null); } else if (content.includes("review") || content.includes("diff")) { setInputSuggestion(" and apply the changes"); } else if (input.startsWith("build") || input.startsWith("create")) { setInputSuggestion(" a React component"); } else if (input.startsWith("refactor") || input.startsWith("optimize")) { setInputSuggestion(" the codebase"); } else { setInputSuggestion(null); } }, [input, messages, isRunning]); /* ── Timeline ── */ const [timelineEvents, setTimelineEvents] = useState([]); const timelineEventIdRef = useRef(0); /* ── Plan mode ── */ const [planSteps, setPlanSteps] = useState([]); const [pendingInput, setPendingInput] = useState(null); /* ── Debug mode state ── */ const [debugError, setDebugError] = useState(null); const [debugFixes, setDebugFixes] = useState([]); /* ── Review mode state ── */ const [reviewFiles, setReviewFiles] = useState([]); const [reviewOverallStatus, setReviewOverallStatus] = useState< "pending" | "approved" | "changes-requested" >("pending"); /* ── IDE workspace state ── */ const [workspaceFiles, setWorkspaceFiles] = useState([ { name: "src", path: "src", type: "folder", children: [] }, { name: "package.json", path: "package.json", type: "file" }, ]); const [terminalLines, setTerminalLines] = useState([ { id: "welcome", type: "system", text: "Wren Terminal v1.0 — Type 'help' for available commands", timestamp: Date.now(), }, ]); /* ── Refs ── */ const inputRef = useRef(null); const messagesEndRef = useRef(null); /* ── WebSocket hook ── */ const { connectWebSocket, sendCommand } = useConversationWebSocket({ state: conversation ? { conversationId: conversationId!, ...conversation, sandboxStatus: conversation.sandboxStatus, } : null, handlers: { onMessage: (msg) => setMessages((prev) => prev.some((m) => m.id === msg.id) ? prev : [...prev, msg], ), onUpdateMessage: (updater) => setMessages(updater), onTimelineEvent: (event) => { setTimelineEvents((prev) => [ ...prev, { ...event, id: `tl-${timelineEventIdRef.current++}`, timestamp: new Date(), }, ]); // Extract agent thinking from action events with thoughts const detail = event.detail; if (detail && event.type === "action") { setThinkingSteps((prev) => [ ...prev, { id: `think-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, timestamp: Date.now(), type: "reasoning", title: event.title || "Agent reasoning", content: detail, }, ]); } }, onError: (err) => { setError(err || null); if (err) { setThinkingSteps((prev) => [ ...prev, { id: `think-err-${Date.now()}`, timestamp: Date.now(), type: "error", title: "Error", content: err || "An error occurred", }, ]); } }, onRunningChange: setIsRunning, onModeSuggested: (mode) => setSelectedMode(mode), onModeFlash: (mode) => { setAutoModeFlash(mode); setTimeout(() => setAutoModeFlash(null), 2000); }, onPlanSteps: setPlanSteps, onDebugError: setDebugError, onDebugFixes: setDebugFixes, onReviewFile: (file) => setReviewFiles((prev) => prev.some((f) => f.path === file.path) ? prev : [...prev, file], ), onWorkspaceFile: (path) => setWorkspaceFiles((prev) => prev.some((f) => f.path === path) ? prev : [ ...prev, { name: path.split("/").pop() || path, path, type: "file" as const, }, ], ), onTerminalLine: (line) => setTerminalLines((prev) => [...prev, line]), onArtifactsCode: (code) => artifacts.setCode(code), onArtifactsTerminal: (text) => artifacts.appendTerminal(text), onArtifactsOpen: () => artifacts.setOpen(true), }, selectedMode, autoMode, reviewFiles, }); /* ── Auto-suggest mode ── */ useEffect(() => { if (input && !isRunning) { const suggested = suggestMode(input); if (suggested && suggested !== selectedMode) { setSelectedMode(suggested); } } }, [input, isRunning, selectedMode]); /* ── Scroll to bottom ── */ const scrollToBottom = useCallback(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, []); useEffect(() => { scrollToBottom(); }, [messages, scrollToBottom]); /* ── Initialize connection ── */ useEffect(() => { if (!conversationId) return; let mounted = true; async function init() { try { setIsConnecting(true); setError(null); const token = localStorage.getItem("token"); const response = await fetch( `/api/v1/app-conversations/${conversationId}`, { headers: { Authorization: `Bearer ${token}` }, }, ); if (!mounted) return; if (response.ok) { const data = await response.json(); const convState = { sessionApiKey: data.session_api_key || "", agentServerUrl: data.conversation_url || "", sandboxStatus: data.sandbox_status, executionStatus: data.execution_status, title: data.title, }; setConversation(convState); if (data.sandbox_status === "RUNNING") { connectWebSocket(convState.agentServerUrl, convState.sessionApiKey); } else { setIsConnecting(false); setNeedsResume(true); } } else if (response.status === 404) { setError("Conversation not found"); setIsConnecting(false); } else { setError("Failed to load conversation"); setIsConnecting(false); } } catch { if (mounted) { setError("Failed to load conversation"); setIsConnecting(false); } } } init(); return () => { mounted = false; }; }, [conversationId, connectWebSocket]); /* ── Send message ── */ const sendMessage = useCallback( async (text: string) => { if (!text.trim() || !conversationId || isRunning) return; const trimmed = text.trim(); setInput(""); // Plan mode: hold as pending for approval if (selectedMode === "plan") { setPendingInput(trimmed); setPlanSteps([ { id: "step-1", title: "Analyze request", description: `Understanding: "${trimmed.slice(0, 100)}"`, files: [], riskLevel: "low", }, { id: "step-2", title: "Identify affected files", description: "Scan codebase for relevant files and dependencies", files: [], riskLevel: "medium", }, ]); setMessages((prev) => [ ...prev, { id: `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, role: "user", content: trimmed, timestamp: new Date(), mode: selectedMode, }, ]); return; } // Reset thinking + mode-specific state on new message setThinkingSteps([]); if (selectedMode === "debug") { setDebugError(null); setDebugFixes([]); } if (selectedMode === "review") { setReviewFiles([]); setReviewOverallStatus("pending"); } setIsRunning(true); setError(null); setMessages((prev) => [ ...prev, { id: `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, role: "user", content: trimmed, timestamp: new Date(), mode: selectedMode, }, ]); try { await ConversationApi.sendMessage(conversationId, trimmed); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send message"); setIsRunning(false); } }, [conversationId, isRunning, selectedMode], ); /* ── Plan approve/reject ── */ const handlePlanApprove = useCallback(async () => { if (!pendingInput || !conversationId) return; setPlanSteps([]); setPendingInput(null); setIsRunning(true); setError(null); try { await ConversationApi.sendMessage(conversationId, pendingInput); } catch (err) { setError(err instanceof Error ? err.message : "Failed to send message"); setIsRunning(false); } }, [pendingInput, conversationId]); const handlePlanReject = useCallback(() => { setPlanSteps([]); setPendingInput(null); setIsRunning(false); setMessages((prev) => [ ...prev, { id: `${Date.now()}-sys`, role: "system", content: "Plan rejected. Rephrase your request.", timestamp: new Date(), }, ]); }, []); /* ── Autonomous mode: auto-approve plan ── */ useEffect(() => { if ( selectedMode === "autonomous" && planSteps.length > 0 && pendingInput && !isRunning ) { setPlanSteps([]); setPendingInput(null); setIsRunning(true); setError(null); ConversationApi.sendMessage(conversationId!, input).catch((err) => { setError(err instanceof Error ? err.message : "Failed to send message"); setIsRunning(false); }); } }, [selectedMode, planSteps.length, pendingInput, isRunning, conversationId]); /* ── Input handlers ── */ const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(input); } }, [sendMessage, input], ); const handleInput = useCallback( (e: React.ChangeEvent) => { setInput(e.target.value); const el = e.target; el.style.height = "auto"; el.style.height = `${Math.min(el.scrollHeight, 200)}px`; }, [], ); /* ── Resume ── */ const handleResume = useCallback(async () => { if ( !conversationId || !conversation?.agentServerUrl || !conversation?.sessionApiKey ) return; setIsResuming(true); setError(null); try { await ConversationApi.resumeConversation(conversationId); connectWebSocket(conversation.agentServerUrl, conversation.sessionApiKey); setNeedsResume(false); } catch (err) { setError(err instanceof Error ? err.message : "Failed to resume"); } finally { setIsResuming(false); } }, [conversationId, conversation, connectWebSocket]); const isCodeMode = selectedMode === "code" || selectedMode === "vibe-code"; const handleTerminalCommand = useCallback( (cmd: string) => { setTerminalLines((prev) => [ ...prev, { id: `cmd-${Date.now()}`, type: "input", text: `$ ${cmd}`, timestamp: Date.now(), }, ]); sendCommand(cmd); }, [sendCommand], ); /* ── Loading ── */ if (isConnecting && !conversation) { return (

Loading conversation...

); } return (
{isCodeMode ? ( /* ── IDE Layout: sidebar + editor + agent panel ── */
{/* ── Left: Files sidebar ── */}
{ /* open file in editor - placeholder */ }} editable={true} viewMode="tree" />
{/* ── Center: Code editor ── */}
{/* ── Right: Agent panel ── */} {showAgentPanel && (
m.role === "user")?.content || "Build the requested feature"} conversationId={conversationId} />
)}
) : ( /* ── Chat Layout: chat + agent panel ── */
{/* ── Left: Chat Area ── */}
{/* ── Chat messages ── */} {selectedMode === "review" && reviewFiles.length > 0 ? (
{ setReviewFiles((prev) => prev.map((f) => f.path === path ? { ...f, status: "accepted" } : f, ), ); }} onRejectFile={(path) => { setReviewFiles((prev) => prev.map((f) => f.path === path ? { ...f, status: "rejected" } : f, ), ); }} onApproveAll={() => { setReviewOverallStatus("approved"); setReviewFiles((prev) => prev.map((f) => ({ ...f, status: "accepted" as const })), ); }} onRejectAll={() => { setReviewOverallStatus("changes-requested"); setReviewFiles((prev) => prev.map((f) => ({ ...f, status: "rejected" as const })), ); }} overallStatus={reviewOverallStatus} />
) : selectedMode === "debug" && debugError ? (
) : (
{/* Timeline (collapsible) */} {timelineEvents.length > 0 && (
)} {/* Thinking / Chain-of-Thought panel */} {thinkingSteps.length > 0 && (
setThinkingCollapsed((c) => !c)} />
)} {/* Plan steps shown inline for plan/debug */} {planSteps.length > 0 && (
)} {messages.length === 0 ? ( <>

I'm ready to help. What would you like me to work on?

Build a React component for a glassmorphism card

) : ( messages.map((message) => ( )) )} {isRunning && (
{[0, 1, 2].map((i) => ( ))}
Thinking
)}
)} {/* Error banner */} {error && (
{error}
)} {/* ── Toggleable Terminal (chat mode) ── */} {showChatTerminal && (
)} {/* ── Chat input ── */}
{ if (inputSuggestion) { setInput((prev) => prev + inputSuggestion); setInputSuggestion(null); // Focus back after accepting setTimeout(() => inputRef.current?.focus(), 0); } }} onChange={handleInput} onKeyDown={handleKeyDown} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} placeholder={isRunning ? "Agent is working..." : "Type a message..."} rows={1} disabled={isRunning || planSteps.length > 0} className="w-full resize-none border-none bg-transparent px-4 py-3.5 text-sm leading-relaxed outline-none placeholder:select-none" style={{ color: "var(--text-primary)", caretColor: "var(--accent)", }} />
{autoModeFlash && (
Auto
)}
{/* Resume banner */} {(needsResume || (conversation?.sandboxStatus && conversation.sandboxStatus !== "RUNNING")) && (

{conversation?.title || "Conversation"} is not running

{conversation?.sandboxStatus === "MISSING" ? "The sandbox no longer exists. Start a new conversation to continue." : "Click Resume to reconnect and continue where you left off."}

)}
)}
); }