aboutsummaryrefslogtreecommitdiffstats
path: root/renderer/src/App.tsx
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-03-04 21:21:22 -0800
committerbndw <ben@bdw.to>2026-03-04 21:21:22 -0800
commitb6405dd6a4ba65fc5dc6746db7be7be7d0bb29f3 (patch)
tree7d04268e9adfe9a6a83029556ef0dd5f72a63d42 /renderer/src/App.tsx
parentead65fd7d50ead785f437cc895c74146bd232702 (diff)
feat: replace header dropdowns with collapsible sidebar tree
- Add Sidebar.tsx: project/session tree with inline rename, collapse/resize - App.tsx: load all sessions at startup, sync selectedProject on session click - Header.tsx: strip project/session UI, keep only right-side controls - globals.css: add .main-layout, sidebar, item, and activity-dot styles - Chat pane: move toggle button to left, use triangle icons matching sidebar
Diffstat (limited to 'renderer/src/App.tsx')
-rw-r--r--renderer/src/App.tsx347
1 files changed, 227 insertions, 120 deletions
diff --git a/renderer/src/App.tsx b/renderer/src/App.tsx
index b2cd168..719faac 100644
--- a/renderer/src/App.tsx
+++ b/renderer/src/App.tsx
@@ -1,5 +1,6 @@
1import React, { useState, useEffect } from "react"; 1import React, { useState, useEffect, useRef } from "react";
2import { Header } from "./components/Header"; 2import { Header } from "./components/Header";
3import { Sidebar } from "./components/Sidebar";
3import { DocumentPane } from "./components/DocumentPane"; 4import { DocumentPane } from "./components/DocumentPane";
4import { ChatPane } from "./components/ChatPane"; 5import { ChatPane } from "./components/ChatPane";
5import { ActionBar } from "./components/ActionBar"; 6import { ActionBar } from "./components/ActionBar";
@@ -51,8 +52,14 @@ export function App() {
51 const [messages, setMessages] = useState<Message[]>([]); 52 const [messages, setMessages] = useState<Message[]>([]);
52 const [documentContent, setDocumentContent] = useState<string>(""); 53 const [documentContent, setDocumentContent] = useState<string>("");
53 const [originalContent, setOriginalContent] = useState<string>(""); 54 const [originalContent, setOriginalContent] = useState<string>("");
54 const [isLoading, setIsLoading] = useState(false); 55 // Per-session loading/activity state so switching sessions doesn't inherit
55 const [activityStatus, setActivityStatus] = useState<string | null>(null); 56 // another session's "Thinking" indicator.
57 const [loadingBySession, setLoadingBySession] = useState<Record<string, boolean>>({});
58 const [activityBySession, setActivityBySession] = useState<Record<string, string | null>>({});
59
60 const isLoading = selectedSession ? (loadingBySession[selectedSession.id] ?? false) : false;
61 const activityStatus = selectedSession ? (activityBySession[selectedSession.id] ?? null) : null;
62
56 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ 63 const [tokenUsage, setTokenUsage] = useState<TokenUsage>({
57 inputTokens: 0, 64 inputTokens: 0,
58 outputTokens: 0, 65 outputTokens: 0,
@@ -65,12 +72,42 @@ export function App() {
65 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark" 72 () => (localStorage.getItem("cf-theme") as Theme) ?? "dark"
66 ); 73 );
67 74
75 // Which phase's artifact is currently displayed in the DocumentPane.
76 // Defaults to the session's current phase; can be overridden to view
77 // a historical artifact (e.g. research.md while in implement phase).
78 const [viewPhase, setViewPhase] = useState<Phase>(
79 (selectedSession?.phase ?? "research") as Phase
80 );
81
82 // Reset viewPhase whenever the active session or its phase changes.
83 useEffect(() => {
84 setViewPhase((selectedSession?.phase ?? "research") as Phase);
85 }, [selectedSession?.id, selectedSession?.phase]);
86
87 const isViewingHistorical =
88 !!selectedSession && viewPhase !== (selectedSession.phase as Phase);
89
90 // Refs so the global claude:message handler always sees the latest values
91 // without needing to re-subscribe when the selected session changes.
92 const selectedSessionRef = useRef(selectedSession);
93 const selectedProjectRef = useRef(selectedProject);
94 const viewPhaseRef = useRef(viewPhase);
95 useEffect(() => { selectedSessionRef.current = selectedSession; }, [selectedSession]);
96 useEffect(() => { selectedProjectRef.current = selectedProject; }, [selectedProject]);
97 useEffect(() => { viewPhaseRef.current = viewPhase; }, [viewPhase]);
98
68 const [chatWidth, setChatWidth] = useState<number>( 99 const [chatWidth, setChatWidth] = useState<number>(
69 () => Number(localStorage.getItem("cf-chat-width")) || 380 100 () => Number(localStorage.getItem("cf-chat-width")) || 380
70 ); 101 );
71 const [chatCollapsed, setChatCollapsed] = useState<boolean>( 102 const [chatCollapsed, setChatCollapsed] = useState<boolean>(
72 () => localStorage.getItem("cf-chat-collapsed") === "true" 103 () => localStorage.getItem("cf-chat-collapsed") === "true"
73 ); 104 );
105 const [sidebarWidth, setSidebarWidth] = useState<number>(
106 () => Number(localStorage.getItem("cf-sidebar-width")) || 260
107 );
108 const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(
109 () => localStorage.getItem("cf-sidebar-collapsed") === "true"
110 );
74 111
75 // Keep document.documentElement in sync and persist to localStorage 112 // Keep document.documentElement in sync and persist to localStorage
76 useEffect(() => { 113 useEffect(() => {
@@ -86,11 +123,20 @@ export function App() {
86 localStorage.setItem("cf-chat-collapsed", String(chatCollapsed)); 123 localStorage.setItem("cf-chat-collapsed", String(chatCollapsed));
87 }, [chatCollapsed]); 124 }, [chatCollapsed]);
88 125
126 useEffect(() => {
127 localStorage.setItem("cf-sidebar-width", String(sidebarWidth));
128 }, [sidebarWidth]);
129
130 useEffect(() => {
131 localStorage.setItem("cf-sidebar-collapsed", String(sidebarCollapsed));
132 }, [sidebarCollapsed]);
133
89 const handleCancel = () => { 134 const handleCancel = () => {
90 if (!selectedSession) return; 135 if (!selectedSession) return;
91 api.interruptSession(selectedSession.id); 136 const id = selectedSession.id;
92 setIsLoading(false); 137 api.interruptSession(id);
93 setActivityStatus(null); 138 setLoadingBySession((prev) => ({ ...prev, [id]: false }));
139 setActivityBySession((prev) => ({ ...prev, [id]: null }));
94 }; 140 };
95 141
96 const handleToggleTheme = () => 142 const handleToggleTheme = () =>
@@ -113,6 +159,23 @@ export function App() {
113 document.addEventListener("mouseup", onUp); 159 document.addEventListener("mouseup", onUp);
114 }; 160 };
115 161
162 const handleSidebarResizeMouseDown = (e: React.MouseEvent) => {
163 e.preventDefault();
164 const startX = e.clientX;
165 const startWidth = sidebarWidth;
166 const onMove = (ev: MouseEvent) => {
167 const delta = ev.clientX - startX; // drag right = wider sidebar
168 const next = Math.max(180, Math.min(400, startWidth + delta));
169 setSidebarWidth(next);
170 };
171 const onUp = () => {
172 document.removeEventListener("mousemove", onMove);
173 document.removeEventListener("mouseup", onUp);
174 };
175 document.addEventListener("mousemove", onMove);
176 document.addEventListener("mouseup", onUp);
177 };
178
116 const hasChanges = documentContent !== originalContent; 179 const hasChanges = documentContent !== originalContent;
117 180
118 // Clear error after 5 seconds 181 // Clear error after 5 seconds
@@ -128,9 +191,10 @@ export function App() {
128 const handleKeyDown = (e: KeyboardEvent) => { 191 const handleKeyDown = (e: KeyboardEvent) => {
129 // Escape to interrupt 192 // Escape to interrupt
130 if (e.key === "Escape" && isLoading && selectedSession) { 193 if (e.key === "Escape" && isLoading && selectedSession) {
131 api.interruptSession(selectedSession.id); 194 const id = selectedSession.id;
132 setIsLoading(false); 195 api.interruptSession(id);
133 setActivityStatus(null); 196 setLoadingBySession((prev) => ({ ...prev, [id]: false }));
197 setActivityBySession((prev) => ({ ...prev, [id]: null }));
134 } 198 }
135 // Cmd/Ctrl + Enter to submit 199 // Cmd/Ctrl + Enter to submit
136 if ( 200 if (
@@ -148,87 +212,93 @@ export function App() {
148 return () => window.removeEventListener("keydown", handleKeyDown); 212 return () => window.removeEventListener("keydown", handleKeyDown);
149 }, [selectedSession, isLoading]); 213 }, [selectedSession, isLoading]);
150 214
151 // Load projects and initial model setting on mount 215 // Load projects, all sessions, and initial model setting on mount.
216 // Sessions for every project are fetched in parallel so the sidebar tree
217 // can display all projects and their sessions immediately.
152 useEffect(() => { 218 useEffect(() => {
153 api.listProjects().then(setProjects); 219 (async () => {
154 // Seed the model badge from the DB so it shows before any query fires. 220 const loadedProjects = await api.listProjects();
155 // system:init will overwrite this with the SDK-resolved name once a query runs. 221 setProjects(loadedProjects);
156 api.getSettings(["model"]).then((s) => { 222 const arrays = await Promise.all(
223 loadedProjects.map((p) => api.listSessions(p.id))
224 );
225 setSessions(arrays.flat());
226 // Seed the model badge from the DB so it shows before any query fires.
227 // system:init will overwrite this with the SDK-resolved name once a query runs.
228 const s = await api.getSettings(["model"]);
157 if (s["model"]) setActiveModel(s["model"]); 229 if (s["model"]) setActiveModel(s["model"]);
158 }); 230 })();
159 }, []); 231 }, []);
160 232
161 // Load sessions when project changes 233 // Load messages when session changes (not affected by viewPhase).
162 useEffect(() => { 234 useEffect(() => {
163 if (selectedProject) { 235 if (selectedSession) {
164 api.listSessions(selectedProject.id).then(setSessions); 236 api.listMessages(selectedSession.id).then(setMessages);
165 } else { 237 } else {
166 setSessions([]); 238 setMessages([]);
167 } 239 }
168 }, [selectedProject]); 240 }, [selectedSession?.id]);
169 241
170 // Load messages and artifact when session changes 242 // Load the viewed artifact whenever the session or the viewed phase changes.
171 useEffect(() => { 243 useEffect(() => {
172 if (selectedSession && selectedProject) { 244 if (selectedSession && selectedProject) {
173 // Load messages 245 const filename = viewPhase === "research" ? "research.md" : "plan.md";
174 api.listMessages(selectedSession.id).then(setMessages);
175
176 // Load session-specific artifact (from ~/.claude-flow/)
177 const filename =
178 selectedSession.phase === "research" ? "research.md" : "plan.md";
179 api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => { 246 api.readSessionArtifact(selectedProject.id, selectedSession.id, filename).then((content) => {
180 const text = content || ""; 247 const text = content || "";
181 setDocumentContent(text); 248 setDocumentContent(text);
182 setOriginalContent(text); 249 setOriginalContent(text);
183 }); 250 });
184 } else { 251 } else {
185 setMessages([]);
186 setDocumentContent(""); 252 setDocumentContent("");
187 setOriginalContent(""); 253 setOriginalContent("");
188 } 254 }
189 }, [selectedSession?.id, selectedSession?.phase, selectedProject]); 255 }, [selectedSession?.id, selectedProject, viewPhase]);
190 256
191 // Subscribe to Claude messages 257 // Subscribe to Claude messages once globally.
258 // Loading/activity state is updated for ANY session (via per-session maps).
259 // Content state (messages, artifacts) is only updated for the currently
260 // selected session, using refs to avoid re-subscribing on every session switch.
192 useEffect(() => { 261 useEffect(() => {
193 const unsubscribe = api.onClaudeMessage((sessionId, msg) => { 262 const unsubscribe = api.onClaudeMessage((sessionId, msg) => {
194 if (sessionId !== selectedSession?.id) return;
195
196 // ── Live activity indicator ────────────────────────────────────── 263 // ── Live activity indicator ──────────────────────────────────────
197 // Primary: tool_progress fires periodically while a tool is running
198 if (msg.type === "tool_progress") { 264 if (msg.type === "tool_progress") {
199 const elapsed = Math.round(msg.elapsed_time_seconds); 265 const elapsed = Math.round(msg.elapsed_time_seconds);
200 setActivityStatus( 266 setActivityBySession((prev) => ({
201 elapsed > 0 267 ...prev,
202 ? `Using ${msg.tool_name} (${elapsed}s)` 268 [sessionId]:
203 : `Using ${msg.tool_name}…` 269 elapsed > 0
204 ); 270 ? `Using ${msg.tool_name} (${elapsed}s)`
271 : `Using ${msg.tool_name}…`,
272 }));
205 } 273 }
206 274
207 // Secondary: task_progress carries last_tool_name for sub-agent tasks
208 if ( 275 if (
209 msg.type === "system" && 276 msg.type === "system" &&
210 msg.subtype === "task_progress" && 277 msg.subtype === "task_progress" &&
211 (msg as { last_tool_name?: string }).last_tool_name 278 (msg as { last_tool_name?: string }).last_tool_name
212 ) { 279 ) {
213 setActivityStatus( 280 setActivityBySession((prev) => ({
214 `Using ${(msg as { last_tool_name: string }).last_tool_name}…` 281 ...prev,
215 ); 282 [sessionId]: `Using ${(msg as { last_tool_name: string }).last_tool_name}…`,
283 }));
216 } 284 }
217 285
218 // ── Model resolved by SDK ──────────────────────────────────────── 286 // ── Model resolved by SDK ────────────────────────────────────────
219 // SDKSystemMessage (subtype "init") contains the actual model in use.
220 if (msg.type === "system" && msg.subtype === "init") { 287 if (msg.type === "system" && msg.subtype === "init") {
221 setActiveModel((msg as { model?: string }).model ?? null); 288 setActiveModel((msg as { model?: string }).model ?? null);
222 } 289 }
223 290
224 // ── Result (success or error) ──────────────────────────────────── 291 // ── Result (success or error) ────────────────────────────────────
225 // Always clear loading state on any result subtype so error results 292 // Clear loading state for the session that completed, regardless of
226 // don't leave the UI stuck in the loading/thinking state. 293 // which session is currently selected.
227 if (msg.type === "result") { 294 if (msg.type === "result") {
228 setIsLoading(false); 295 setLoadingBySession((prev) => ({ ...prev, [sessionId]: false }));
229 setActivityStatus(null); 296 setActivityBySession((prev) => ({ ...prev, [sessionId]: null }));
230 297
231 if (msg.subtype === "success") { 298 // Content updates only matter for the currently visible session.
299 const currentSession = selectedSessionRef.current;
300 const currentProject = selectedProjectRef.current;
301 if (sessionId === currentSession?.id && msg.subtype === "success") {
232 if (msg.usage) { 302 if (msg.usage) {
233 setTokenUsage({ 303 setTokenUsage({
234 inputTokens: msg.usage.input_tokens, 304 inputTokens: msg.usage.input_tokens,
@@ -236,20 +306,24 @@ export function App() {
236 cacheHits: msg.usage.cache_read_input_tokens, 306 cacheHits: msg.usage.cache_read_input_tokens,
237 }); 307 });
238 } 308 }
239 // Reload artifact after Claude updates it 309 // Only reload the artifact if the user is viewing the current phase's
240 if (selectedProject && selectedSession) { 310 // document. If they're browsing a historical artifact, leave it alone;
311 // the effect tied to viewPhase will reload when they navigate back.
312 if (
313 currentProject &&
314 currentSession &&
315 viewPhaseRef.current === currentSession.phase
316 ) {
241 const filename = 317 const filename =
242 selectedSession.phase === "research" ? "research.md" : "plan.md"; 318 currentSession.phase === "research" ? "research.md" : "plan.md";
243 // Capture for the async closure 319 const sessionAtTrigger = currentSession;
244 const sessionAtTrigger = selectedSession;
245 api 320 api
246 .readSessionArtifact(selectedProject.id, sessionAtTrigger.id, filename) 321 .readSessionArtifact(currentProject.id, sessionAtTrigger.id, filename)
247 .then((content) => { 322 .then((content) => {
248 const text = content || ""; 323 const text = content || "";
249 setDocumentContent(text); 324 setDocumentContent(text);
250 setOriginalContent(text); 325 setOriginalContent(text);
251 326
252 // Auto-name: only during research, only while name is still the default
253 if ( 327 if (
254 sessionAtTrigger.phase === "research" && 328 sessionAtTrigger.phase === "research" &&
255 /^Session \d+$/.test(sessionAtTrigger.name) && 329 /^Session \d+$/.test(sessionAtTrigger.name) &&
@@ -266,7 +340,8 @@ export function App() {
266 } 340 }
267 341
268 // ── Assistant message ──────────────────────────────────────────── 342 // ── Assistant message ────────────────────────────────────────────
269 if (msg.type === "assistant") { 343 // Only update the chat pane for the currently visible session.
344 if (msg.type === "assistant" && sessionId === selectedSessionRef.current?.id) {
270 const content = ( 345 const content = (
271 msg.message.content as Array<{ type: string; text?: string }> 346 msg.message.content as Array<{ type: string; text?: string }>
272 ) 347 )
@@ -278,10 +353,8 @@ export function App() {
278 setMessages((prev) => { 353 setMessages((prev) => {
279 const last = prev[prev.length - 1]; 354 const last = prev[prev.length - 1];
280 if (last?.role === "assistant") { 355 if (last?.role === "assistant") {
281 // Update existing assistant message
282 return [...prev.slice(0, -1), { ...last, content }]; 356 return [...prev.slice(0, -1), { ...last, content }];
283 } 357 }
284 // Add new assistant message
285 return [ 358 return [
286 ...prev, 359 ...prev,
287 { 360 {
@@ -298,74 +371,74 @@ export function App() {
298 }); 371 });
299 372
300 return unsubscribe; 373 return unsubscribe;
301 }, [selectedSession?.id, selectedSession?.phase, selectedProject]); 374 }, []); // Subscribe once — uses refs for current session/project
302 375
303 const handleSendMessage = async (message: string) => { 376 const handleSendMessage = async (message: string) => {
304 if (!selectedSession) return; 377 if (!selectedSession) return;
305 setIsLoading(true); 378 const id = selectedSession.id;
379 setLoadingBySession((prev) => ({ ...prev, [id]: true }));
306 setError(null); 380 setError(null);
307 setMessages((prev) => [ 381 setMessages((prev) => [
308 ...prev, 382 ...prev,
309 { 383 {
310 id: crypto.randomUUID(), 384 id: crypto.randomUUID(),
311 session_id: selectedSession.id, 385 session_id: id,
312 role: "user", 386 role: "user",
313 content: message, 387 content: message,
314 created_at: Date.now() / 1000, 388 created_at: Date.now() / 1000,
315 }, 389 },
316 ]); 390 ]);
317 try { 391 try {
318 await api.sendMessage(selectedSession.id, message); 392 await api.sendMessage(id, message);
319 } catch (err) { 393 } catch (err) {
320 setError(err instanceof Error ? err.message : "Failed to send message"); 394 setError(err instanceof Error ? err.message : "Failed to send message");
321 setIsLoading(false); 395 setLoadingBySession((prev) => ({ ...prev, [id]: false }));
322 } 396 }
323 }; 397 };
324 398
325 const handleReview = async () => { 399 const handleReview = async () => {
326 if (!selectedSession || !selectedProject) return; 400 if (!selectedSession || !selectedProject || isViewingHistorical) return;
401 const id = selectedSession.id;
327 setError(null); 402 setError(null);
328 try { 403 try {
329 // Save user edits first (session-specific, stored in ~/.claude-flow/)
330 const filename = 404 const filename =
331 selectedSession.phase === "research" ? "research.md" : "plan.md"; 405 selectedSession.phase === "research" ? "research.md" : "plan.md";
332 await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent); 406 await api.writeSessionArtifact(selectedProject.id, id, filename, documentContent);
333 setOriginalContent(documentContent); 407 setOriginalContent(documentContent);
334 setIsLoading(true); 408 setLoadingBySession((prev) => ({ ...prev, [id]: true }));
335 await api.triggerReview(selectedSession.id); 409 await api.triggerReview(id);
336 } catch (err) { 410 } catch (err) {
337 setError(err instanceof Error ? err.message : "Review failed"); 411 setError(err instanceof Error ? err.message : "Review failed");
338 setIsLoading(false); 412 setLoadingBySession((prev) => ({ ...prev, [id]: false }));
339 } 413 }
340 }; 414 };
341 415
342 const handleSubmit = async () => { 416 const handleSubmit = async () => {
343 if (!selectedSession || !selectedProject) return; 417 if (!selectedSession || !selectedProject || isViewingHistorical) return;
418 const id = selectedSession.id;
344 setError(null); 419 setError(null);
345 try { 420 try {
346 // Save any pending edits (session-specific, stored in ~/.claude-flow/)
347 const filename = 421 const filename =
348 selectedSession.phase === "research" ? "research.md" : "plan.md"; 422 selectedSession.phase === "research" ? "research.md" : "plan.md";
349 await api.writeSessionArtifact(selectedProject.id, selectedSession.id, filename, documentContent); 423 await api.writeSessionArtifact(selectedProject.id, id, filename, documentContent);
350 424
351 const advanced = await api.advancePhase(selectedSession.id); 425 const advanced = await api.advancePhase(id);
352 if (advanced) { 426 if (advanced) {
353 setSelectedSession({ 427 setSelectedSession({
354 ...selectedSession, 428 ...selectedSession,
355 phase: advanced.phase, 429 phase: advanced.phase,
356 git_branch: advanced.git_branch, 430 git_branch: advanced.git_branch,
357 }); 431 });
358 // Trigger initial message for next phase 432 setLoadingBySession((prev) => ({ ...prev, [id]: true }));
359 setIsLoading(true);
360 const initialMsg = 433 const initialMsg =
361 advanced.phase === "plan" 434 advanced.phase === "plan"
362 ? "Create a detailed implementation plan based on the research." 435 ? "Create a detailed implementation plan based on the research."
363 : "Begin implementing the plan."; 436 : "Begin implementing the plan.";
364 await api.sendMessage(selectedSession.id, initialMsg); 437 await api.sendMessage(id, initialMsg);
365 } 438 }
366 } catch (err) { 439 } catch (err) {
367 setError(err instanceof Error ? err.message : "Submit failed"); 440 setError(err instanceof Error ? err.message : "Submit failed");
368 setIsLoading(false); 441 setLoadingBySession((prev) => ({ ...prev, [id]: false }));
369 } 442 }
370 }; 443 };
371 444
@@ -378,26 +451,28 @@ export function App() {
378 setSelectedProject(project); 451 setSelectedProject(project);
379 }; 452 };
380 453
381 const handleCreateSession = async () => { 454 const handleCreateSession = async (projectId: string) => {
382 if (!selectedProject) return; 455 const project = projects.find((p) => p.id === projectId);
383 const name = `Session ${sessions.length + 1}`; 456 if (!project) return;
384 const session = await api.createSession(selectedProject.id, name); 457 const projectSessions = sessions.filter((s) => s.project_id === projectId);
458 const name = `Session ${projectSessions.length + 1}`;
459 const session = await api.createSession(projectId, name);
385 setSessions((prev) => [session, ...prev]); 460 setSessions((prev) => [session, ...prev]);
461 setSelectedProject(project);
386 setSelectedSession(session); 462 setSelectedSession(session);
387 setMessages([]); 463 setMessages([]);
388 setDocumentContent(""); 464 setDocumentContent("");
389 setOriginalContent(""); 465 setOriginalContent("");
390 // Note: Each session has its own artifact directory, no need to clear
391 }; 466 };
392 467
393 const handleDeleteProject = async (id: string) => { 468 const handleDeleteProject = async (id: string) => {
394 try { 469 try {
395 await api.deleteProject(id); 470 await api.deleteProject(id);
396 setProjects((prev) => prev.filter((p) => p.id !== id)); 471 setProjects((prev) => prev.filter((p) => p.id !== id));
472 setSessions((prev) => prev.filter((s) => s.project_id !== id));
397 if (selectedProject?.id === id) { 473 if (selectedProject?.id === id) {
398 setSelectedProject(null); 474 setSelectedProject(null);
399 setSelectedSession(null); 475 setSelectedSession(null);
400 setSessions([]);
401 setMessages([]); 476 setMessages([]);
402 setDocumentContent(""); 477 setDocumentContent("");
403 setOriginalContent(""); 478 setOriginalContent("");
@@ -436,56 +511,88 @@ export function App() {
436 return ( 511 return (
437 <div className="app"> 512 <div className="app">
438 <Header 513 <Header
439 projects={projects}
440 sessions={sessions}
441 selectedProject={selectedProject}
442 selectedSession={selectedSession} 514 selectedSession={selectedSession}
443 onSelectProject={setSelectedProject}
444 onSelectSession={setSelectedSession}
445 onCreateProject={handleCreateProject}
446 onCreateSession={handleCreateSession}
447 onDeleteProject={handleDeleteProject}
448 onDeleteSession={handleDeleteSession}
449 onRenameSession={handleRenameSession}
450 theme={theme} 515 theme={theme}
451 onToggleTheme={handleToggleTheme} 516 onToggleTheme={handleToggleTheme}
452 gitBranch={selectedSession?.git_branch ?? null} 517 gitBranch={selectedSession?.git_branch ?? null}
453 onOpenSettings={() => setShowSettings(true)} 518 onOpenSettings={() => setShowSettings(true)}
519 viewPhase={viewPhase}
520 onViewPhase={setViewPhase}
454 /> 521 />
455 522
456 <div className="main-content"> 523 <div className="main-layout">
457 <DocumentPane 524 <Sidebar
458 content={documentContent} 525 projects={projects}
459 onChange={setDocumentContent} 526 sessions={sessions}
460 phase={selectedSession?.phase || "research"} 527 selectedProject={selectedProject}
461 disabled={!selectedSession || selectedSession.phase === "implement"} 528 selectedSession={selectedSession}
462 showOnboarding={!selectedProject} 529 onSelectProject={(p) => {
463 theme={theme} 530 setSelectedProject(p);
531 setSelectedSession(null);
532 }}
533 onSelectSession={(session) => {
534 if (session) {
535 const project = projects.find((p) => p.id === session.project_id);
536 if (project) setSelectedProject(project);
537 }
538 setSelectedSession(session);
539 }}
540 onCreateProject={handleCreateProject}
541 onCreateSession={handleCreateSession}
542 onDeleteProject={handleDeleteProject}
543 onDeleteSession={handleDeleteSession}
544 onRenameSession={handleRenameSession}
545 loadingBySession={loadingBySession}
546 width={sidebarWidth}
547 collapsed={sidebarCollapsed}
548 onCollapsedChange={setSidebarCollapsed}
464 /> 549 />
465 550
466 {!chatCollapsed && ( 551 {!sidebarCollapsed && (
467 <div 552 <div
468 className="chat-resize-handle" 553 className="sidebar-resize-handle"
469 onMouseDown={handleResizeMouseDown} 554 onMouseDown={handleSidebarResizeMouseDown}
470 /> 555 />
471 )} 556 )}
472 557
473 <ChatPane 558 <div className="main-content">
474 messages={messages} 559 <DocumentPane
475 onSend={handleSendMessage} 560 content={documentContent}
476 isLoading={isLoading} 561 onChange={setDocumentContent}
477 disabled={!selectedSession} 562 phase={viewPhase}
478 placeholder={ 563 disabled={
479 selectedSession 564 !selectedSession ||
480 ? `Chat with Claude (${selectedSession.phase})...` 565 isViewingHistorical ||
481 : "Select a session to start" 566 selectedSession.phase === "implement"
482 } 567 }
483 collapsed={chatCollapsed} 568 showOnboarding={!selectedProject}
484 chatWidth={chatWidth} 569 theme={theme}
485 onToggleCollapse={() => setChatCollapsed((c) => !c)} 570 />
486 activityStatus={activityStatus} 571
487 onCancel={handleCancel} 572 {!chatCollapsed && (
488 /> 573 <div
574 className="chat-resize-handle"
575 onMouseDown={handleResizeMouseDown}
576 />
577 )}
578
579 <ChatPane
580 messages={messages}
581 onSend={handleSendMessage}
582 isLoading={isLoading}
583 disabled={!selectedSession}
584 placeholder={
585 selectedSession
586 ? `Chat with Claude (${selectedSession.phase})...`
587 : "Select a session to start"
588 }
589 collapsed={chatCollapsed}
590 chatWidth={chatWidth}
591 onToggleCollapse={() => setChatCollapsed((c) => !c)}
592 activityStatus={activityStatus}
593 onCancel={handleCancel}
594 />
595 </div>
489 </div> 596 </div>
490 597
491 {error && ( 598 {error && (