# Plan: Claude Flow Implementation ## Overview Build an Electron app that wraps the Claude Agent SDK with an opinionated workflow: Research → Plan → Annotate → Implement. --- ## Directory Structure (Target) ``` claude-flow/ ├── src/main/ │ ├── index.ts # App lifecycle, window management │ ├── preload.ts # IPC bridge │ ├── db/ │ │ ├── index.ts # Database connection singleton │ │ ├── schema.ts # Table definitions + migrations │ │ ├── projects.ts # Project CRUD │ │ └── sessions.ts # Session CRUD │ ├── claude/ │ │ ├── index.ts # Claude SDK wrapper │ │ ├── phases.ts # Phase configs (prompts, tools, permissions) │ │ └── hooks.ts # Custom hooks for phase enforcement │ └── ipc/ │ ├── index.ts # Register all handlers │ ├── projects.ts # Project IPC handlers │ ├── sessions.ts # Session IPC handlers │ └── claude.ts # Claude IPC handlers ├── renderer/ │ ├── index.html │ └── src/ │ ├── main.tsx # React entry │ ├── App.tsx # Main app component │ ├── components/ │ │ ├── Sidebar.tsx # Project/session tree │ │ ├── SessionList.tsx # Sessions for a project │ │ ├── Chat.tsx # Message thread │ │ ├── ChatInput.tsx # Input box │ │ ├── Message.tsx # Single message bubble │ │ ├── ArtifactPane.tsx # Markdown editor for plan/research │ │ ├── PhaseBar.tsx # Phase indicator + controls │ │ └── Settings.tsx # API key, preferences │ ├── hooks/ │ │ ├── useProjects.ts │ │ ├── useSessions.ts │ │ └── useChat.ts │ ├── lib/ │ │ └── api.ts # Typed IPC wrapper │ ├── types.ts # Shared types │ └── styles/ │ └── globals.css ├── package.json ├── tsconfig.json └── vite.config.ts ``` --- ## Phase 1: Database Layer ### 1.1 Schema (`src/main/db/schema.ts`) ```typescript import Database from "better-sqlite3"; export function initSchema(db: Database.Database) { db.exec(` CREATE TABLE IF NOT EXISTS projects ( id TEXT PRIMARY KEY, name TEXT NOT NULL, path TEXT NOT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, phase TEXT NOT NULL DEFAULT 'research', claude_session_id TEXT, permission_mode TEXT NOT NULL DEFAULT 'acceptEdits', created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, role TEXT NOT NULL, content TEXT NOT NULL, tool_use TEXT, created_at INTEGER NOT NULL DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id); `); } ``` **Notes:** - `claude_session_id` stores the SDK's session ID for resuming - `permission_mode` is user toggle: `acceptEdits` or `bypassPermissions` - `phase` is one of: `research`, `plan`, `annotate`, `implement` - Messages stored for display; actual context in SDK session ### 1.2 Database Connection (`src/main/db/index.ts`) ```typescript import Database from "better-sqlite3"; import { app } from "electron"; import path from "node:path"; import fs from "node:fs"; import { initSchema } from "./schema"; let db: Database.Database | null = null; export function getDb(): Database.Database { if (db) return db; const dbDir = app.getPath("userData"); if (!fs.existsSync(dbDir)) { fs.mkdirSync(dbDir, { recursive: true }); } const dbPath = path.join(dbDir, "claude-flow.db"); db = new Database(dbPath); db.pragma("journal_mode = WAL"); db.pragma("foreign_keys = ON"); initSchema(db); return db; } export function closeDb() { if (db) { db.close(); db = null; } } ``` ### 1.3 Project CRUD (`src/main/db/projects.ts`) ```typescript import { getDb } from "./index"; import { v4 as uuid } from "uuid"; export interface Project { id: string; name: string; path: string; created_at: number; updated_at: number; } export function listProjects(): Project[] { const db = getDb(); return db.prepare("SELECT * FROM projects ORDER BY updated_at DESC").all() as Project[]; } export function getProject(id: string): Project | undefined { const db = getDb(); return db.prepare("SELECT * FROM projects WHERE id = ?").get(id) as Project | undefined; } export function createProject(name: string, projectPath: string): Project { const db = getDb(); const id = uuid(); const now = Math.floor(Date.now() / 1000); db.prepare( "INSERT INTO projects (id, name, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?)" ).run(id, name, projectPath, now, now); return { id, name, path: projectPath, created_at: now, updated_at: now }; } export function updateProject(id: string, updates: Partial>): void { const db = getDb(); const sets: string[] = []; const values: any[] = []; if (updates.name !== undefined) { sets.push("name = ?"); values.push(updates.name); } if (updates.path !== undefined) { sets.push("path = ?"); values.push(updates.path); } if (sets.length > 0) { sets.push("updated_at = ?"); values.push(Math.floor(Date.now() / 1000)); values.push(id); db.prepare(`UPDATE projects SET ${sets.join(", ")} WHERE id = ?`).run(...values); } } export function deleteProject(id: string): void { const db = getDb(); db.prepare("DELETE FROM projects WHERE id = ?").run(id); } ``` ### 1.4 Session CRUD (`src/main/db/sessions.ts`) ```typescript import { getDb } from "./index"; import { v4 as uuid } from "uuid"; export type Phase = "research" | "plan" | "annotate" | "implement"; export type PermissionMode = "acceptEdits" | "bypassPermissions"; export interface Session { id: string; project_id: string; name: string; phase: Phase; claude_session_id: string | null; permission_mode: PermissionMode; created_at: number; updated_at: number; } export interface Message { id: string; session_id: string; role: "user" | "assistant" | "system"; content: string; tool_use: string | null; created_at: number; } export function listSessions(projectId: string): Session[] { const db = getDb(); return db .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC") .all(projectId) as Session[]; } export function getSession(id: string): Session | undefined { const db = getDb(); return db.prepare("SELECT * FROM sessions WHERE id = ?").get(id) as Session | undefined; } export function createSession(projectId: string, name: string): Session { const db = getDb(); const id = uuid(); const now = Math.floor(Date.now() / 1000); db.prepare( `INSERT INTO sessions (id, project_id, name, phase, permission_mode, created_at, updated_at) VALUES (?, ?, ?, 'research', 'acceptEdits', ?, ?)` ).run(id, projectId, name, now, now); return { id, project_id: projectId, name, phase: "research", claude_session_id: null, permission_mode: "acceptEdits", created_at: now, updated_at: now, }; } export function updateSession( id: string, updates: Partial> ): void { const db = getDb(); const sets: string[] = []; const values: any[] = []; for (const [key, value] of Object.entries(updates)) { if (value !== undefined) { sets.push(`${key} = ?`); values.push(value); } } if (sets.length > 0) { sets.push("updated_at = ?"); values.push(Math.floor(Date.now() / 1000)); values.push(id); db.prepare(`UPDATE sessions SET ${sets.join(", ")} WHERE id = ?`).run(...values); } } export function deleteSession(id: string): void { const db = getDb(); db.prepare("DELETE FROM sessions WHERE id = ?").run(id); } // Messages export function listMessages(sessionId: string): Message[] { const db = getDb(); return db .prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC") .all(sessionId) as Message[]; } export function addMessage( sessionId: string, role: Message["role"], content: string, toolUse?: string ): Message { const db = getDb(); const id = uuid(); const now = Math.floor(Date.now() / 1000); db.prepare( "INSERT INTO messages (id, session_id, role, content, tool_use, created_at) VALUES (?, ?, ?, ?, ?, ?)" ).run(id, sessionId, role, content, toolUse ?? null, now); // Touch session updated_at db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId); return { id, session_id: sessionId, role, content, tool_use: toolUse ?? null, created_at: now }; } ``` --- ## Phase 2: Claude Integration ### 2.1 Phase Configs (`src/main/claude/phases.ts`) ```typescript import { Phase, PermissionMode } from "../db/sessions"; export interface PhaseConfig { systemPrompt: string; allowedTools: string[]; permissionMode: "plan" | PermissionMode; } export const phaseConfigs: Record = { research: { permissionMode: "plan", allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch"], systemPrompt: `You are in RESEARCH mode. Your job is to deeply understand the relevant parts of the codebase before any changes are made. Rules: - Read files thoroughly — "deeply", "in great detail", understand all intricacies - Write all findings to .claude-flow/research.md - DO NOT make any code changes - DO NOT modify any files except .claude-flow/research.md When finished, summarize what you learned and wait for the next instruction.`, }, plan: { permissionMode: "plan", allowedTools: ["Read", "Glob", "Grep", "Write"], systemPrompt: `You are in PLANNING mode. Your job is to write a detailed implementation plan based on the research. Rules: - Write the plan to .claude-flow/plan.md - Include specific code snippets showing proposed changes - Include file paths that will be modified - Include trade-offs and considerations - Add a granular TODO list at the end with phases and tasks - DO NOT implement anything - DO NOT modify any source files The plan should be detailed enough that implementation becomes mechanical.`, }, annotate: { permissionMode: "plan", allowedTools: ["Read", "Write"], systemPrompt: `You are in ANNOTATION mode. The human has added notes/comments to .claude-flow/plan.md. Rules: - Read .claude-flow/plan.md carefully - Find all inline notes (marked with comments, "// NOTE:", "REVIEW:", etc.) - Address each note by updating the relevant section - DO NOT implement anything - DO NOT modify any source files - Only update .claude-flow/plan.md When finished, summarize the changes you made to the plan.`, }, implement: { permissionMode: "acceptEdits", // Will be overridden by user setting allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved. Rules: - Follow .claude-flow/plan.md exactly - Mark tasks complete in the plan as you finish them - Run typecheck/lint continuously if available - Do not add unnecessary comments or jsdocs - Do not use any or unknown types - Do not stop until all tasks are complete If you encounter issues not covered by the plan, stop and ask.`, }, }; export function getPhaseConfig(phase: Phase, userPermissionMode?: PermissionMode): PhaseConfig { const config = { ...phaseConfigs[phase] }; // In implement phase, use user's permission mode preference if (phase === "implement" && userPermissionMode) { config.permissionMode = userPermissionMode; } return config; } ``` ### 2.2 Hooks (`src/main/claude/hooks.ts`) ```typescript import { HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk"; import { Phase } from "../db/sessions"; export function createPhaseEnforcementHook(getPhase: () => Phase): HookCallback { return async (input, toolUseID, { signal }) => { if (input.hook_event_name !== "PreToolUse") return {}; const preInput = input as PreToolUseHookInput; const phase = getPhase(); const toolInput = preInput.tool_input as Record; const filePath = toolInput?.file_path as string | undefined; // In non-implement phases, only allow writes to .claude-flow/ artifacts if (phase !== "implement" && ["Write", "Edit"].includes(preInput.tool_name)) { if (filePath && !filePath.includes(".claude-flow/")) { return { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: `Cannot modify ${filePath} in ${phase} phase. Only .claude-flow/ artifacts allowed.`, }, }; } } return {}; }; } ``` ### 2.3 Claude Wrapper (`src/main/claude/index.ts`) ```typescript import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { Session, Phase, updateSession } from "../db/sessions"; import { getPhaseConfig } from "./phases"; import { createPhaseEnforcementHook } from "./hooks"; import { getProject } from "../db/projects"; // Active queries by session ID const activeQueries = new Map>(); export interface SendMessageOptions { session: Session; message: string; onMessage: (msg: SDKMessage) => void; } export async function sendMessage({ session, message, onMessage }: SendMessageOptions): Promise { const project = getProject(session.project_id); if (!project) throw new Error("Project not found"); const phaseConfig = getPhaseConfig(session.phase, session.permission_mode); // Create phase getter that reads current session state const getPhase = (): Phase => { // In a real app, read from DB; here we use closure return session.phase; }; const q = query({ prompt: message, options: { cwd: project.path, resume: session.claude_session_id ?? undefined, systemPrompt: phaseConfig.systemPrompt, allowedTools: phaseConfig.allowedTools, permissionMode: phaseConfig.permissionMode, hooks: { PreToolUse: [ { hooks: [createPhaseEnforcementHook(getPhase)] }, ], }, }, }); activeQueries.set(session.id, q); try { for await (const msg of q) { // Capture session ID from init message if (msg.type === "system" && msg.subtype === "init") { if (!session.claude_session_id) { updateSession(session.id, { claude_session_id: msg.session_id }); session.claude_session_id = msg.session_id; } } onMessage(msg); } } finally { activeQueries.delete(session.id); } } export function interruptSession(sessionId: string): void { const q = activeQueries.get(sessionId); if (q) { q.close(); activeQueries.delete(sessionId); } } export async function setSessionPhase(session: Session, phase: Phase): Promise { updateSession(session.id, { phase }); // If there's an active query, update its permission mode const q = activeQueries.get(session.id); if (q && phase === "implement") { await q.setPermissionMode(session.permission_mode); } } ``` --- ## Phase 3: IPC Layer ### 3.1 Preload (`src/main/preload.ts`) ```typescript import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"; export interface ClaudeFlowAPI { // Projects listProjects: () => Promise; createProject: (name: string, path: string) => Promise; deleteProject: (id: string) => Promise; // Sessions listSessions: (projectId: string) => Promise; getSession: (id: string) => Promise; createSession: (projectId: string, name: string) => Promise; deleteSession: (id: string) => Promise; setPhase: (sessionId: string, phase: string) => Promise; setPermissionMode: (sessionId: string, mode: string) => Promise; // Messages listMessages: (sessionId: string) => Promise; sendMessage: (sessionId: string, message: string) => Promise; interruptSession: (sessionId: string) => Promise; // Events onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void; // Dialogs selectDirectory: () => Promise; } contextBridge.exposeInMainWorld("api", { // Projects listProjects: () => ipcRenderer.invoke("projects:list"), createProject: (name: string, path: string) => ipcRenderer.invoke("projects:create", name, path), deleteProject: (id: string) => ipcRenderer.invoke("projects:delete", id), // Sessions listSessions: (projectId: string) => ipcRenderer.invoke("sessions:list", projectId), getSession: (id: string) => ipcRenderer.invoke("sessions:get", id), createSession: (projectId: string, name: string) => ipcRenderer.invoke("sessions:create", projectId, name), deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id), setPhase: (sessionId: string, phase: string) => ipcRenderer.invoke("sessions:setPhase", sessionId, phase), setPermissionMode: (sessionId: string, mode: string) => ipcRenderer.invoke("sessions:setPermissionMode", sessionId, mode), // Messages listMessages: (sessionId: string) => ipcRenderer.invoke("messages:list", sessionId), sendMessage: (sessionId: string, message: string) => ipcRenderer.invoke("claude:send", sessionId, message), interruptSession: (sessionId: string) => ipcRenderer.invoke("claude:interrupt", sessionId), // Events onClaudeMessage: (callback: (sessionId: string, message: any) => void) => { const handler = (_event: IpcRendererEvent, sessionId: string, message: any) => { callback(sessionId, message); }; ipcRenderer.on("claude:message", handler); return () => ipcRenderer.removeListener("claude:message", handler); }, // Dialogs selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"), } satisfies ClaudeFlowAPI); ``` ### 3.2 IPC Handlers (`src/main/ipc/index.ts`) ```typescript import { ipcMain, dialog, BrowserWindow } from "electron"; import * as projects from "../db/projects"; import * as sessions from "../db/sessions"; import * as claude from "../claude"; export function registerIpcHandlers(mainWindow: BrowserWindow) { // Projects ipcMain.handle("projects:list", () => projects.listProjects()); ipcMain.handle("projects:create", (_, name: string, path: string) => projects.createProject(name, path) ); ipcMain.handle("projects:delete", (_, id: string) => projects.deleteProject(id)); // Sessions ipcMain.handle("sessions:list", (_, projectId: string) => sessions.listSessions(projectId)); ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id)); ipcMain.handle("sessions:create", (_, projectId: string, name: string) => sessions.createSession(projectId, name) ); ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id)); ipcMain.handle("sessions:setPhase", async (_, sessionId: string, phase: string) => { const session = sessions.getSession(sessionId); if (session) { await claude.setSessionPhase(session, phase as sessions.Phase); } }); ipcMain.handle("sessions:setPermissionMode", (_, sessionId: string, mode: string) => { sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode }); }); // Messages ipcMain.handle("messages:list", (_, sessionId: string) => sessions.listMessages(sessionId)); ipcMain.handle("claude:send", async (_, sessionId: string, message: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); // Store user message sessions.addMessage(sessionId, "user", message); // Send to Claude await claude.sendMessage({ session, message, onMessage: (msg) => { // Forward messages to renderer mainWindow.webContents.send("claude:message", sessionId, msg); // Store assistant messages if (msg.type === "assistant") { const content = msg.message.content .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join("\n"); if (content) { sessions.addMessage(sessionId, "assistant", content); } } }, }); }); ipcMain.handle("claude:interrupt", (_, sessionId: string) => { claude.interruptSession(sessionId); }); // Dialogs ipcMain.handle("dialog:selectDirectory", async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"], }); return result.canceled ? null : result.filePaths[0]; }); } ``` ### 3.3 Update Main Entry (`src/main/index.ts`) ```typescript import { app, BrowserWindow } from "electron"; import path from "node:path"; import { getDb, closeDb } from "./db"; import { registerIpcHandlers } from "./ipc"; const isDev = !app.isPackaged; let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 800, minHeight: 600, show: false, webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, "preload.js"), }, }); // Register IPC handlers registerIpcHandlers(mainWindow); if (isDev) { const url = process.env.VITE_DEV_SERVER_URL ?? "http://localhost:5173"; mainWindow.loadURL(url).finally(() => { mainWindow!.show(); mainWindow!.webContents.openDevTools({ mode: "detach" }); }); } else { const indexHtml = path.join(app.getAppPath(), "renderer", "dist", "index.html"); mainWindow.loadFile(indexHtml).finally(() => mainWindow!.show()); } } app.whenReady().then(() => { // Initialize database getDb(); createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on("window-all-closed", () => { closeDb(); if (process.platform !== "darwin") { app.quit(); } }); ``` --- ## Phase 4: React UI ### 4.1 Types (`renderer/src/types.ts`) ```typescript export interface Project { id: string; name: string; path: string; created_at: number; updated_at: number; } export interface Session { id: string; project_id: string; name: string; phase: Phase; claude_session_id: string | null; permission_mode: PermissionMode; created_at: number; updated_at: number; } export type Phase = "research" | "plan" | "annotate" | "implement"; export type PermissionMode = "acceptEdits" | "bypassPermissions"; export interface Message { id: string; session_id: string; role: "user" | "assistant" | "system"; content: string; tool_use: string | null; created_at: number; } ``` ### 4.2 API Wrapper (`renderer/src/lib/api.ts`) ```typescript import type { Project, Session, Message, Phase, PermissionMode } from "../types"; declare global { interface Window { api: { listProjects: () => Promise; createProject: (name: string, path: string) => Promise; deleteProject: (id: string) => Promise; listSessions: (projectId: string) => Promise; getSession: (id: string) => Promise; createSession: (projectId: string, name: string) => Promise; deleteSession: (id: string) => Promise; setPhase: (sessionId: string, phase: Phase) => Promise; setPermissionMode: (sessionId: string, mode: PermissionMode) => Promise; listMessages: (sessionId: string) => Promise; sendMessage: (sessionId: string, message: string) => Promise; interruptSession: (sessionId: string) => Promise; onClaudeMessage: (callback: (sessionId: string, message: any) => void) => () => void; selectDirectory: () => Promise; }; } } export const api = window.api; ``` ### 4.3 App Component (`renderer/src/App.tsx`) ```typescript import React, { useState, useEffect } from "react"; import { api } from "./lib/api"; import type { Project, Session } from "./types"; import { Sidebar } from "./components/Sidebar"; import { Chat } from "./components/Chat"; import { PhaseBar } from "./components/PhaseBar"; import "./styles/globals.css"; export function App() { const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [selectedSession, setSelectedSession] = useState(null); useEffect(() => { api.listProjects().then(setProjects); }, []); 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 handleSelectSession = async (session: Session) => { setSelectedSession(session); }; return (
{selectedSession ? ( <> { api.setPhase(selectedSession.id, phase); setSelectedSession({ ...selectedSession, phase }); }} onPermissionModeChange={(mode) => { api.setPermissionMode(selectedSession.id, mode); setSelectedSession({ ...selectedSession, permission_mode: mode }); }} /> ) : (
{selectedProject ? "Select or create a session to start" : "Select or create a project to start"}
)}
); } ``` ### 4.4 Sidebar (`renderer/src/components/Sidebar.tsx`) ```typescript import React, { useState, useEffect } from "react"; import { api } from "../lib/api"; import type { Project, Session } from "../types"; interface SidebarProps { projects: Project[]; selectedProject: Project | null; selectedSession: Session | null; onSelectProject: (project: Project) => void; onSelectSession: (session: Session) => void; onCreateProject: () => void; } export function Sidebar({ projects, selectedProject, selectedSession, onSelectProject, onSelectSession, onCreateProject, }: SidebarProps) { const [sessions, setSessions] = useState([]); useEffect(() => { if (selectedProject) { api.listSessions(selectedProject.id).then(setSessions); } else { setSessions([]); } }, [selectedProject]); const handleCreateSession = async () => { if (!selectedProject) return; const name = `Session ${sessions.length + 1}`; const session = await api.createSession(selectedProject.id, name); setSessions((prev) => [session, ...prev]); onSelectSession(session); }; return ( ); } ``` ### 4.5 PhaseBar (`renderer/src/components/PhaseBar.tsx`) ```typescript import React from "react"; import type { Session, Phase, PermissionMode } from "../types"; interface PhaseBarProps { session: Session; onPhaseChange: (phase: Phase) => void; onPermissionModeChange: (mode: PermissionMode) => void; } const phases: Phase[] = ["research", "plan", "annotate", "implement"]; export function PhaseBar({ session, onPhaseChange, onPermissionModeChange }: PhaseBarProps) { return (
{phases.map((phase) => ( ))}
{session.phase === "implement" && (
)}
); } ``` ### 4.6 Chat (`renderer/src/components/Chat.tsx`) ```typescript import React, { useState, useEffect, useRef } from "react"; import { api } from "../lib/api"; import type { Session, Message } from "../types"; interface ChatProps { session: Session; } export function Chat({ session }: ChatProps) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); // Load messages useEffect(() => { api.listMessages(session.id).then(setMessages); }, [session.id]); // Subscribe to Claude messages useEffect(() => { const unsubscribe = api.onClaudeMessage((sessionId, msg) => { if (sessionId !== session.id) return; if (msg.type === "assistant") { const content = msg.message.content .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join("\n"); if (content) { setMessages((prev) => { // Update last assistant message or add new one const lastMsg = prev[prev.length - 1]; if (lastMsg?.role === "assistant") { return [...prev.slice(0, -1), { ...lastMsg, content }]; } return [ ...prev, { id: crypto.randomUUID(), session_id: session.id, role: "assistant", content, tool_use: null, created_at: Date.now() / 1000, }, ]; }); } } if (msg.type === "result") { setIsLoading(false); } }); return unsubscribe; }, [session.id]); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); const handleSend = async () => { if (!input.trim() || isLoading) return; const userMessage: Message = { id: crypto.randomUUID(), session_id: session.id, role: "user", content: input, tool_use: null, created_at: Date.now() / 1000, }; setMessages((prev) => [...prev, userMessage]); setInput(""); setIsLoading(true); try { await api.sendMessage(session.id, input); } catch (error) { console.error("Failed to send message:", error); setIsLoading(false); } }; return (
{messages.map((msg) => (
{msg.content}
))} {isLoading &&
Claude is thinking...
}