diff options
Diffstat (limited to 'renderer/src/App.tsx')
| -rw-r--r-- | renderer/src/App.tsx | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx new file mode 100644 index 0000000..7a8c378 --- /dev/null +++ b/renderer/src/App.tsx | |||
| @@ -0,0 +1,241 @@ | |||
| 1 | import React, { useState, useEffect, useCallback } from "react"; | ||
| 2 | import { Header } from "./components/Header"; | ||
| 3 | import { DocumentPane } from "./components/DocumentPane"; | ||
| 4 | import { ChatPane } from "./components/ChatPane"; | ||
| 5 | import { ActionBar } from "./components/ActionBar"; | ||
| 6 | import type { Project, Session, Message, Phase, TokenUsage } from "./types"; | ||
| 7 | import "./styles/globals.css"; | ||
| 8 | |||
| 9 | const api = window.api; | ||
| 10 | |||
| 11 | export function App() { | ||
| 12 | const [projects, setProjects] = useState<Project[]>([]); | ||
| 13 | const [sessions, setSessions] = useState<Session[]>([]); | ||
| 14 | const [selectedProject, setSelectedProject] = useState<Project | null>(null); | ||
| 15 | const [selectedSession, setSelectedSession] = useState<Session | null>(null); | ||
| 16 | const [messages, setMessages] = useState<Message[]>([]); | ||
| 17 | const [documentContent, setDocumentContent] = useState<string>(""); | ||
| 18 | const [originalContent, setOriginalContent] = useState<string>(""); | ||
| 19 | const [isLoading, setIsLoading] = useState(false); | ||
| 20 | const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ | ||
| 21 | inputTokens: 0, | ||
| 22 | outputTokens: 0, | ||
| 23 | }); | ||
| 24 | |||
| 25 | const hasChanges = documentContent !== originalContent; | ||
| 26 | |||
| 27 | // Load projects on mount | ||
| 28 | useEffect(() => { | ||
| 29 | api.listProjects().then(setProjects); | ||
| 30 | }, []); | ||
| 31 | |||
| 32 | // Load sessions when project changes | ||
| 33 | useEffect(() => { | ||
| 34 | if (selectedProject) { | ||
| 35 | api.listSessions(selectedProject.id).then(setSessions); | ||
| 36 | } else { | ||
| 37 | setSessions([]); | ||
| 38 | } | ||
| 39 | }, [selectedProject]); | ||
| 40 | |||
| 41 | // Load messages and artifact when session changes | ||
| 42 | useEffect(() => { | ||
| 43 | if (selectedSession && selectedProject) { | ||
| 44 | // Load messages | ||
| 45 | api.listMessages(selectedSession.id).then(setMessages); | ||
| 46 | |||
| 47 | // Load artifact | ||
| 48 | const filename = | ||
| 49 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | ||
| 50 | api.readArtifact(selectedProject.path, filename).then((content) => { | ||
| 51 | const text = content || ""; | ||
| 52 | setDocumentContent(text); | ||
| 53 | setOriginalContent(text); | ||
| 54 | }); | ||
| 55 | } else { | ||
| 56 | setMessages([]); | ||
| 57 | setDocumentContent(""); | ||
| 58 | setOriginalContent(""); | ||
| 59 | } | ||
| 60 | }, [selectedSession?.id, selectedSession?.phase, selectedProject]); | ||
| 61 | |||
| 62 | // Subscribe to Claude messages | ||
| 63 | useEffect(() => { | ||
| 64 | const unsubscribe = api.onClaudeMessage((sessionId, msg) => { | ||
| 65 | if (sessionId !== selectedSession?.id) return; | ||
| 66 | |||
| 67 | if (msg.type === "result" && msg.subtype === "success") { | ||
| 68 | setIsLoading(false); | ||
| 69 | if (msg.usage) { | ||
| 70 | setTokenUsage({ | ||
| 71 | inputTokens: msg.usage.input_tokens, | ||
| 72 | outputTokens: msg.usage.output_tokens, | ||
| 73 | cacheHits: msg.usage.cache_read_input_tokens, | ||
| 74 | }); | ||
| 75 | } | ||
| 76 | // Reload artifact after Claude updates it | ||
| 77 | if (selectedProject) { | ||
| 78 | const filename = | ||
| 79 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | ||
| 80 | api.readArtifact(selectedProject.path, filename).then((content) => { | ||
| 81 | const text = content || ""; | ||
| 82 | setDocumentContent(text); | ||
| 83 | setOriginalContent(text); | ||
| 84 | }); | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | if (msg.type === "assistant") { | ||
| 89 | const content = ( | ||
| 90 | msg.message.content as Array<{ type: string; text?: string }> | ||
| 91 | ) | ||
| 92 | .filter((c) => c.type === "text" && c.text) | ||
| 93 | .map((c) => c.text!) | ||
| 94 | .join("\n"); | ||
| 95 | |||
| 96 | if (content) { | ||
| 97 | setMessages((prev) => { | ||
| 98 | const last = prev[prev.length - 1]; | ||
| 99 | if (last?.role === "assistant") { | ||
| 100 | // Update existing assistant message | ||
| 101 | return [...prev.slice(0, -1), { ...last, content }]; | ||
| 102 | } | ||
| 103 | // Add new assistant message | ||
| 104 | return [ | ||
| 105 | ...prev, | ||
| 106 | { | ||
| 107 | id: crypto.randomUUID(), | ||
| 108 | session_id: sessionId, | ||
| 109 | role: "assistant", | ||
| 110 | content, | ||
| 111 | created_at: Date.now() / 1000, | ||
| 112 | }, | ||
| 113 | ]; | ||
| 114 | }); | ||
| 115 | } | ||
| 116 | } | ||
| 117 | }); | ||
| 118 | |||
| 119 | return unsubscribe; | ||
| 120 | }, [selectedSession?.id, selectedSession?.phase, selectedProject]); | ||
| 121 | |||
| 122 | const handleSendMessage = async (message: string) => { | ||
| 123 | if (!selectedSession) return; | ||
| 124 | setIsLoading(true); | ||
| 125 | setMessages((prev) => [ | ||
| 126 | ...prev, | ||
| 127 | { | ||
| 128 | id: crypto.randomUUID(), | ||
| 129 | session_id: selectedSession.id, | ||
| 130 | role: "user", | ||
| 131 | content: message, | ||
| 132 | created_at: Date.now() / 1000, | ||
| 133 | }, | ||
| 134 | ]); | ||
| 135 | await api.sendMessage(selectedSession.id, message); | ||
| 136 | }; | ||
| 137 | |||
| 138 | const handleReview = async () => { | ||
| 139 | if (!selectedSession || !selectedProject) return; | ||
| 140 | // Save user edits first | ||
| 141 | const filename = | ||
| 142 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | ||
| 143 | await api.writeArtifact(selectedProject.path, filename, documentContent); | ||
| 144 | setOriginalContent(documentContent); | ||
| 145 | setIsLoading(true); | ||
| 146 | await api.triggerReview(selectedSession.id); | ||
| 147 | }; | ||
| 148 | |||
| 149 | const handleSubmit = async () => { | ||
| 150 | if (!selectedSession || !selectedProject) return; | ||
| 151 | // Save any pending edits | ||
| 152 | const filename = | ||
| 153 | selectedSession.phase === "research" ? "research.md" : "plan.md"; | ||
| 154 | await api.writeArtifact(selectedProject.path, filename, documentContent); | ||
| 155 | |||
| 156 | const nextPhase = await api.advancePhase(selectedSession.id); | ||
| 157 | if (nextPhase) { | ||
| 158 | setSelectedSession({ ...selectedSession, phase: nextPhase }); | ||
| 159 | // Trigger initial message for next phase | ||
| 160 | setIsLoading(true); | ||
| 161 | const initialMsg = | ||
| 162 | nextPhase === "plan" | ||
| 163 | ? "Create a detailed implementation plan based on the research." | ||
| 164 | : "Begin implementing the plan."; | ||
| 165 | await api.sendMessage(selectedSession.id, initialMsg); | ||
| 166 | } | ||
| 167 | }; | ||
| 168 | |||
| 169 | const handleCreateProject = async () => { | ||
| 170 | const path = await api.selectDirectory(); | ||
| 171 | if (!path) return; | ||
| 172 | const name = path.split("/").pop() || "New Project"; | ||
| 173 | const project = await api.createProject(name, path); | ||
| 174 | setProjects((prev) => [project, ...prev]); | ||
| 175 | setSelectedProject(project); | ||
| 176 | }; | ||
| 177 | |||
| 178 | const handleCreateSession = async () => { | ||
| 179 | if (!selectedProject) return; | ||
| 180 | const name = `Session ${sessions.length + 1}`; | ||
| 181 | const session = await api.createSession(selectedProject.id, name); | ||
| 182 | setSessions((prev) => [session, ...prev]); | ||
| 183 | setSelectedSession(session); | ||
| 184 | setMessages([]); | ||
| 185 | setDocumentContent(""); | ||
| 186 | setOriginalContent(""); | ||
| 187 | }; | ||
| 188 | |||
| 189 | return ( | ||
| 190 | <div className="app"> | ||
| 191 | <Header | ||
| 192 | projects={projects} | ||
| 193 | sessions={sessions} | ||
| 194 | selectedProject={selectedProject} | ||
| 195 | selectedSession={selectedSession} | ||
| 196 | onSelectProject={setSelectedProject} | ||
| 197 | onSelectSession={setSelectedSession} | ||
| 198 | onCreateProject={handleCreateProject} | ||
| 199 | onCreateSession={handleCreateSession} | ||
| 200 | /> | ||
| 201 | |||
| 202 | <div className="main-content"> | ||
| 203 | <DocumentPane | ||
| 204 | content={documentContent} | ||
| 205 | onChange={setDocumentContent} | ||
| 206 | phase={selectedSession?.phase || "research"} | ||
| 207 | disabled={!selectedSession || selectedSession.phase === "implement"} | ||
| 208 | /> | ||
| 209 | |||
| 210 | <ChatPane | ||
| 211 | messages={messages} | ||
| 212 | onSend={handleSendMessage} | ||
| 213 | isLoading={isLoading} | ||
| 214 | disabled={!selectedSession} | ||
| 215 | placeholder={ | ||
| 216 | selectedSession | ||
| 217 | ? `Chat with Claude (${selectedSession.phase})...` | ||
| 218 | : "Select a session to start" | ||
| 219 | } | ||
| 220 | /> | ||
| 221 | </div> | ||
| 222 | |||
| 223 | <ActionBar | ||
| 224 | phase={selectedSession?.phase || "research"} | ||
| 225 | hasChanges={hasChanges} | ||
| 226 | isLoading={isLoading} | ||
| 227 | tokenUsage={tokenUsage} | ||
| 228 | permissionMode={selectedSession?.permission_mode || "acceptEdits"} | ||
| 229 | onReview={handleReview} | ||
| 230 | onSubmit={handleSubmit} | ||
| 231 | onPermissionModeChange={(mode) => { | ||
| 232 | if (selectedSession) { | ||
| 233 | api.setPermissionMode(selectedSession.id, mode); | ||
| 234 | setSelectedSession({ ...selectedSession, permission_mode: mode }); | ||
| 235 | } | ||
| 236 | }} | ||
| 237 | disabled={!selectedSession} | ||
| 238 | /> | ||
| 239 | </div> | ||
| 240 | ); | ||
| 241 | } | ||
