aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/App.tsx
diff options
context:
space:
mode:
authorClawd <ai@clawd.bot>2026-02-28 07:30:40 -0800
committerClawd <ai@clawd.bot>2026-02-28 07:30:40 -0800
commit6d70c5f8a3ed90564b08616a3fb041409916059c (patch)
tree4c6480c5a606ab0a178dca72efa93c60ebe70c34 /renderer/src/App.tsx
parent66f66d1c17213f55aa56d69c0cccc309b16f3362 (diff)
Phase 4: React UI
- Add renderer/src/types.ts with Project, Session, Message, Phase types - Add renderer/src/styles/globals.css with full styling - Dark theme with accent colors - Header, document pane, chat pane, action bar layouts - Phase indicator, token usage bar, buttons - Add renderer/src/components/Header.tsx - Project/session dropdowns with create buttons - Phase indicator showing current workflow state - Add renderer/src/components/DocumentPane.tsx - Markdown viewer/editor with toggle - Syntax highlighting for review comments - Task checkbox rendering - Add renderer/src/components/ChatPane.tsx - Message list with auto-scroll - Input field with Enter to send - Loading state indicator - Add renderer/src/components/ActionBar.tsx - Token usage bar with color coding - Review/Submit buttons for workflow - Permission mode toggle for implement phase - Add renderer/src/App.tsx - Full state management for projects, sessions, messages - Claude message subscription - Workflow handlers (review, submit, phase advance) - Update renderer/src/main.tsx to render App
Diffstat (limited to 'renderer/src/App.tsx')
-rw-r--r--renderer/src/App.tsx241
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 @@
1import React, { useState, useEffect, useCallback } from "react";
2import { Header } from "./components/Header";
3import { DocumentPane } from "./components/DocumentPane";
4import { ChatPane } from "./components/ChatPane";
5import { ActionBar } from "./components/ActionBar";
6import type { Project, Session, Message, Phase, TokenUsage } from "./types";
7import "./styles/globals.css";
8
9const api = window.api;
10
11export 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}