import React, { useState, useEffect, useCallback } from "react"; import { Header } from "./components/Header"; import { DocumentPane } from "./components/DocumentPane"; import { ChatPane } from "./components/ChatPane"; import { ActionBar } from "./components/ActionBar"; import type { Project, Session, Message, Phase, TokenUsage } from "./types"; import "./styles/globals.css"; const api = window.api; export function App() { const [projects, setProjects] = useState([]); const [sessions, setSessions] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [selectedSession, setSelectedSession] = useState(null); const [messages, setMessages] = useState([]); const [documentContent, setDocumentContent] = useState(""); const [originalContent, setOriginalContent] = useState(""); const [isLoading, setIsLoading] = useState(false); const [tokenUsage, setTokenUsage] = useState({ inputTokens: 0, outputTokens: 0, }); const hasChanges = documentContent !== originalContent; // Load projects on mount useEffect(() => { api.listProjects().then(setProjects); }, []); // Load sessions when project changes useEffect(() => { if (selectedProject) { api.listSessions(selectedProject.id).then(setSessions); } else { setSessions([]); } }, [selectedProject]); // Load messages and artifact when session changes useEffect(() => { if (selectedSession && selectedProject) { // Load messages api.listMessages(selectedSession.id).then(setMessages); // Load artifact const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; api.readArtifact(selectedProject.path, filename).then((content) => { const text = content || ""; setDocumentContent(text); setOriginalContent(text); }); } else { setMessages([]); setDocumentContent(""); setOriginalContent(""); } }, [selectedSession?.id, selectedSession?.phase, selectedProject]); // Subscribe to Claude messages useEffect(() => { const unsubscribe = api.onClaudeMessage((sessionId, msg) => { if (sessionId !== selectedSession?.id) return; if (msg.type === "result" && msg.subtype === "success") { setIsLoading(false); if (msg.usage) { setTokenUsage({ inputTokens: msg.usage.input_tokens, outputTokens: msg.usage.output_tokens, cacheHits: msg.usage.cache_read_input_tokens, }); } // Reload artifact after Claude updates it if (selectedProject) { const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; api.readArtifact(selectedProject.path, filename).then((content) => { const text = content || ""; setDocumentContent(text); setOriginalContent(text); }); } } if (msg.type === "assistant") { const content = ( msg.message.content as Array<{ type: string; text?: string }> ) .filter((c) => c.type === "text" && c.text) .map((c) => c.text!) .join("\n"); if (content) { setMessages((prev) => { const last = prev[prev.length - 1]; if (last?.role === "assistant") { // Update existing assistant message return [...prev.slice(0, -1), { ...last, content }]; } // Add new assistant message return [ ...prev, { id: crypto.randomUUID(), session_id: sessionId, role: "assistant", content, created_at: Date.now() / 1000, }, ]; }); } } }); return unsubscribe; }, [selectedSession?.id, selectedSession?.phase, selectedProject]); const handleSendMessage = async (message: string) => { if (!selectedSession) return; setIsLoading(true); setMessages((prev) => [ ...prev, { id: crypto.randomUUID(), session_id: selectedSession.id, role: "user", content: message, created_at: Date.now() / 1000, }, ]); await api.sendMessage(selectedSession.id, message); }; const handleReview = async () => { if (!selectedSession || !selectedProject) return; // Save user edits first const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; await api.writeArtifact(selectedProject.path, filename, documentContent); setOriginalContent(documentContent); setIsLoading(true); await api.triggerReview(selectedSession.id); }; const handleSubmit = async () => { if (!selectedSession || !selectedProject) return; // Save any pending edits const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; await api.writeArtifact(selectedProject.path, filename, documentContent); const nextPhase = await api.advancePhase(selectedSession.id); if (nextPhase) { setSelectedSession({ ...selectedSession, phase: nextPhase }); // Trigger initial message for next phase setIsLoading(true); const initialMsg = nextPhase === "plan" ? "Create a detailed implementation plan based on the research." : "Begin implementing the plan."; await api.sendMessage(selectedSession.id, initialMsg); } }; const handleCreateProject = async () => { const path = await api.selectDirectory(); if (!path) return; const name = path.split("/").pop() || "New Project"; const project = await api.createProject(name, path); setProjects((prev) => [project, ...prev]); setSelectedProject(project); }; const handleCreateSession = async () => { if (!selectedProject) return; const name = `Session ${sessions.length + 1}`; const session = await api.createSession(selectedProject.id, name); setSessions((prev) => [session, ...prev]); setSelectedSession(session); setMessages([]); setDocumentContent(""); setOriginalContent(""); }; return (
{ if (selectedSession) { api.setPermissionMode(selectedSession.id, mode); setSelectedSession({ ...selectedSession, permission_mode: mode }); } }} disabled={!selectedSession} />
); }