# Plan: Claude Flow Implementation ## Overview A document-centric coding assistant that enforces a structured workflow: **Research → Plan → Implement**. The primary UI is a **markdown document viewer/editor** with a **chat sidebar**. The workflow is driven by the document, not the chat. --- ## User Flow ### 1. Project Setup - Add a project by selecting a folder - Start a new session within the project ### 2. Research Phase - Chat dialogue: Claude asks what to research and what you want to build - You provide direction via chat - Claude generates `research.md` → displayed as rendered markdown - You edit the document (add comments, adjustments) - App detects changes → enables **[Review]** and **[Submit]** buttons - **Review**: Claude reads your changes and adjusts the document - **Submit**: Move to Plan phase ### 3. Plan Phase - Claude generates `plan.md` based on research - Displayed as rendered markdown - You edit, iterate with **[Review]** - **Submit**: Kicks off Implementation ### 4. Implement Phase - Claude executes the plan - Marks tasks complete as it goes - Chat shows progress and tool usage --- ## UI Layout ``` ┌──────────────────────────────────────────────────────────────┐ │ ┌─────────────────┐ ┌─────────────────┐ [Research ● ───]│ │ │ Project ▾ │ │ Session ▾ │ [Plan ○────────]│ │ └─────────────────┘ └─────────────────┘ [Implement ○───]│ ├────────────────────────────────────────┬─────────────────────┤ │ │ │ │ ┌────────────────────────────────┐ │ Chat Dialogue │ │ │ │ │ │ │ │ # Research Findings │ │ ┌───────────────┐ │ │ │ │ │ │ What areas │ │ │ │ ## Authentication System │ │ │ should I │ │ │ │ │ │ │ research? │ │ │ │ The auth module uses JWT... │ │ └───────────────┘ │ │ │ │ │ │ │ │ // REVIEW: check OAuth too │ │ ┌───────────────┐ │ │ │ │ │ │ Research the │ │ │ │ │ │ │ auth system, │ │ │ │ │ │ │ I want OAuth │ │ │ └────────────────────────────────┘ │ └───────────────┘ │ │ │ │ │ 42k / 200k tokens ████░░░░░░░ │ │ ├────────────────────────────────────────┼─────────────────────┤ │ [Review] [Submit →] │ [____________] ⏎ │ └────────────────────────────────────────┴─────────────────────┘ ``` ### Key UI Elements | Element | Behavior | |---------|----------| | **Project dropdown** | Select/create projects | | **Session dropdown** | Select/create sessions within project | | **Phase indicator** | Shows current phase (Research → Plan → Implement) | | **Document pane** | Rendered markdown, editable | | **Chat pane** | Dialogue with Claude | | **Review button** | Disabled until document edited. Triggers Claude to read changes. | | **Submit button** | Advances to next phase | | **Token indicator** | Shows context usage | --- ## Directory Structure ``` 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/ │ └── handlers.ts # All IPC handlers ├── renderer/ │ ├── index.html │ └── src/ │ ├── main.tsx │ ├── App.tsx │ ├── components/ │ │ ├── Header.tsx # Project/session dropdowns + phase indicator │ │ ├── DocumentPane.tsx # Markdown viewer/editor │ │ ├── ChatPane.tsx # Chat dialogue │ │ ├── ActionBar.tsx # Review/Submit buttons + token indicator │ │ └── Message.tsx # Single chat message │ ├── lib/ │ │ ├── api.ts # Typed IPC wrapper │ │ └── markdown.ts # Markdown rendering utilities │ ├── types.ts │ └── 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, 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:** - `phase` is one of: `research`, `plan`, `implement` - `claude_session_id` stores the SDK's session ID for resuming - `permission_mode`: `acceptEdits` or `bypassPermissions` (user toggle in implement phase) ### 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[] { return getDb() .prepare("SELECT * FROM projects ORDER BY updated_at DESC") .all() as Project[]; } export function getProject(id: string): Project | undefined { return getDb() .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 deleteProject(id: string): void { getDb().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" | "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"; content: string; created_at: number; } export function listSessions(projectId: string): Session[] { return getDb() .prepare("SELECT * FROM sessions WHERE project_id = ? ORDER BY updated_at DESC") .all(projectId) as Session[]; } export function getSession(id: string): Session | undefined { return getDb() .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 { getDb().prepare("DELETE FROM sessions WHERE id = ?").run(id); } // Messages export function listMessages(sessionId: string): Message[] { return getDb() .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): Message { const db = getDb(); const id = uuid(); const now = Math.floor(Date.now() / 1000); db.prepare( "INSERT INTO messages (id, session_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)" ).run(id, sessionId, role, content, now); db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(now, sessionId); return { id, session_id: sessionId, role, content, 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; initialMessage: string; // What Claude says when entering this phase } export const phaseConfigs: Record = { research: { permissionMode: "plan", allowedTools: ["Read", "Glob", "Grep", "WebSearch", "WebFetch", "Write"], initialMessage: "What areas of the codebase should I research? What are you trying to build?", systemPrompt: `You are in RESEARCH mode. Your job is to deeply understand the codebase before any changes are made. When the user tells you what to research: 1. Read files thoroughly — understand all intricacies 2. Write your findings to .claude-flow/research.md 3. Format it as clear, readable markdown Rules: - DO NOT make any code changes - DO NOT modify any files except .claude-flow/research.md - Be thorough — surface-level reading is not acceptable When the user clicks "Review", read .claude-flow/research.md for their annotations and update accordingly. When the user clicks "Submit", they're ready to move to planning.`, }, plan: { permissionMode: "plan", allowedTools: ["Read", "Glob", "Grep", "Write"], initialMessage: "I'll create a detailed implementation plan based on my research. Give me a moment...", systemPrompt: `You are in PLANNING mode. Based on the research in .claude-flow/research.md, write a detailed implementation plan. Write the plan to .claude-flow/plan.md with: - Detailed explanation of the approach - Specific code snippets showing proposed changes - File paths that will be modified - Trade-offs and considerations - A granular TODO list with checkboxes Rules: - DO NOT implement anything - DO NOT modify any source files - Only write to .claude-flow/plan.md The plan should be detailed enough that implementation becomes mechanical. When the user clicks "Review", read .claude-flow/plan.md for their annotations and update accordingly. When the user clicks "Submit", begin implementation.`, }, implement: { permissionMode: "acceptEdits", // Will be overridden by user setting allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], initialMessage: "Starting implementation. I'll follow the plan exactly and mark tasks complete as I go.", systemPrompt: `You are in IMPLEMENTATION mode. The plan has been approved. Read .claude-flow/plan.md and execute it: - Follow the plan exactly - Mark tasks complete (- [x]) as you finish them - Run typecheck/lint continuously if available - Do not add unnecessary comments - 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] }; if (phase === "implement" && userPermissionMode) { config.permissionMode = userPermissionMode; } return config; } ``` ### 2.2 Claude Wrapper (`src/main/claude/index.ts`) ```typescript import { query, SDKMessage } from "@anthropic-ai/claude-agent-sdk"; import { Session, Phase, updateSession, getSession } from "../db/sessions"; import { getPhaseConfig } from "./phases"; import { getProject } from "../db/projects"; import fs from "node:fs"; import path from "node:path"; const activeQueries = new Map>(); function ensureArtifactDir(projectPath: string): void { const dir = path.join(projectPath, ".claude-flow"); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } 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"); ensureArtifactDir(project.path); const phaseConfig = getPhaseConfig(session.phase, session.permission_mode); const q = query({ prompt: message, options: { cwd: project.path, resume: session.claude_session_id ?? undefined, systemPrompt: phaseConfig.systemPrompt, allowedTools: phaseConfig.allowedTools, permissionMode: phaseConfig.permissionMode, }, }); activeQueries.set(session.id, q); try { for await (const msg of q) { if (msg.type === "system" && msg.subtype === "init") { if (!session.claude_session_id) { updateSession(session.id, { 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); } } // Trigger review: Claude reads the document and addresses annotations export async function triggerReview(session: Session, onMessage: (msg: SDKMessage) => void): Promise { const docName = session.phase === "research" ? "research.md" : "plan.md"; const message = `I've updated .claude-flow/${docName} with annotations. Read the file, find all my inline notes (marked with // REVIEW:, // NOTE:, TODO:, or similar), address each one, and update the document accordingly. Do not implement anything yet.`; await sendMessage({ session, message, onMessage }); } // Advance to next phase export function advancePhase(session: Session): Phase | null { const nextPhase: Record = { research: "plan", plan: "implement", implement: null, }; const next = nextPhase[session.phase]; if (next) { updateSession(session.id, { phase: next }); } return next; } // Read artifact file export function readArtifact(projectPath: string, filename: string): string | null { const filePath = path.join(projectPath, ".claude-flow", filename); if (fs.existsSync(filePath)) { return fs.readFileSync(filePath, "utf-8"); } return null; } // Write artifact file (for user edits) export function writeArtifact(projectPath: string, filename: string, content: string): void { const dir = path.join(projectPath, ".claude-flow"); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(path.join(dir, filename), content, "utf-8"); } ``` --- ## Phase 3: IPC Layer ### 3.1 Preload (`src/main/preload.ts`) ```typescript import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"; 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), createSession: (projectId: string, name: string) => ipcRenderer.invoke("sessions:create", projectId, name), deleteSession: (id: string) => ipcRenderer.invoke("sessions:delete", id), getSession: (id: string) => ipcRenderer.invoke("sessions:get", id), // Chat sendMessage: (sessionId: string, message: string) => ipcRenderer.invoke("chat:send", sessionId, message), interruptSession: (sessionId: string) => ipcRenderer.invoke("chat:interrupt", sessionId), // Workflow triggerReview: (sessionId: string) => ipcRenderer.invoke("workflow:review", sessionId), advancePhase: (sessionId: string) => ipcRenderer.invoke("workflow:advance", sessionId), setPermissionMode: (sessionId: string, mode: string) => ipcRenderer.invoke("workflow:setPermissionMode", sessionId, mode), // Artifacts readArtifact: (projectPath: string, filename: string) => ipcRenderer.invoke("artifact:read", projectPath, filename), writeArtifact: (projectPath: string, filename: string, content: string) => ipcRenderer.invoke("artifact:write", projectPath, filename, content), // Events onClaudeMessage: (callback: (sessionId: string, message: any) => void) => { const handler = (_: 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"), }); ``` ### 3.2 IPC Handlers (`src/main/ipc/handlers.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:create", (_, projectId: string, name: string) => sessions.createSession(projectId, name)); ipcMain.handle("sessions:delete", (_, id: string) => sessions.deleteSession(id)); ipcMain.handle("sessions:get", (_, id: string) => sessions.getSession(id)); // Chat ipcMain.handle("chat:send", async (_, sessionId: string, message: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); sessions.addMessage(sessionId, "user", message); await claude.sendMessage({ session, message, onMessage: (msg) => { mainWindow.webContents.send("claude:message", sessionId, msg); 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("chat:interrupt", (_, sessionId: string) => { claude.interruptSession(sessionId); }); // Workflow ipcMain.handle("workflow:review", async (_, sessionId: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); await claude.triggerReview(session, (msg) => { mainWindow.webContents.send("claude:message", sessionId, msg); }); }); ipcMain.handle("workflow:advance", (_, sessionId: string) => { const session = sessions.getSession(sessionId); if (!session) throw new Error("Session not found"); return claude.advancePhase(session); }); ipcMain.handle("workflow:setPermissionMode", (_, sessionId: string, mode: string) => { sessions.updateSession(sessionId, { permission_mode: mode as sessions.PermissionMode }); }); // Artifacts ipcMain.handle("artifact:read", (_, projectPath: string, filename: string) => { return claude.readArtifact(projectPath, filename); }); ipcMain.handle("artifact:write", (_, projectPath: string, filename: string, content: string) => { claude.writeArtifact(projectPath, filename, content); }); // Dialogs ipcMain.handle("dialog:selectDirectory", async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ["openDirectory"] }); return result.canceled ? null : result.filePaths[0]; }); } ``` ### 3.3 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/handlers"; const isDev = !app.isPackaged; let mainWindow: BrowserWindow | null = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, minWidth: 1000, minHeight: 600, show: false, titleBarStyle: "hiddenInset", webPreferences: { contextIsolation: true, nodeIntegration: false, preload: path.join(__dirname, "preload.js"), }, }); 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(() => { 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" | "implement"; export type PermissionMode = "acceptEdits" | "bypassPermissions"; export interface Message { id: string; session_id: string; role: "user" | "assistant"; content: string; created_at: number; } export interface TokenUsage { inputTokens: number; outputTokens: number; cacheHits?: number; } ``` ### 4.2 App Component (`renderer/src/App.tsx`) ```typescript 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, 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 artifact when session/phase changes useEffect(() => { if (selectedSession && selectedProject) { const filename = selectedSession.phase === "research" ? "research.md" : "plan.md"; api.readArtifact(selectedProject.path, filename).then((content) => { const text = content || ""; setDocumentContent(text); setOriginalContent(text); }); } }, [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") { 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 .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join("\n"); if (content) { setMessages((prev) => { const last = prev[prev.length - 1]; if (last?.role === "assistant") { return [...prev.slice(0, -1), { ...last, content }]; } 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} />
); } ``` ### 4.3 Header (`renderer/src/components/Header.tsx`) ```typescript import React from "react"; import type { Project, Session, Phase } from "../types"; interface HeaderProps { projects: Project[]; sessions: Session[]; selectedProject: Project | null; selectedSession: Session | null; onSelectProject: (project: Project | null) => void; onSelectSession: (session: Session | null) => void; onCreateProject: () => void; onCreateSession: () => void; } const phaseLabels: Record = { research: "Research", plan: "Plan", implement: "Implement", }; export function Header({ projects, sessions, selectedProject, selectedSession, onSelectProject, onSelectSession, onCreateProject, onCreateSession, }: HeaderProps) { return (
{selectedProject && ( <> )}
{selectedSession && (
{(["research", "plan", "implement"] as Phase[]).map((phase) => ( {phaseLabels[phase]} ))}
)}
); } ``` ### 4.4 DocumentPane (`renderer/src/components/DocumentPane.tsx`) ```typescript import React, { useMemo } from "react"; import type { Phase } from "../types"; interface DocumentPaneProps { content: string; onChange: (content: string) => void; phase: Phase; disabled: boolean; } // Simple markdown renderer (can be replaced with a library like react-markdown) function renderMarkdown(md: string): string { return md // Headers .replace(/^### (.*$)/gm, '

$1

') .replace(/^## (.*$)/gm, '

$1

') .replace(/^# (.*$)/gm, '

$1

') // Bold/italic .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/\*([^*]+)\*/g, '$1') // Code blocks .replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
') .replace(/`([^`]+)`/g, '$1') // Lists .replace(/^\- \[x\] (.*$)/gm, '
  • ☑ $1
  • ') .replace(/^\- \[ \] (.*$)/gm, '
  • ☐ $1
  • ') .replace(/^\- (.*$)/gm, '
  • $1
  • ') // Review comments (highlight them) .replace(/(\/\/ REVIEW:.*$)/gm, '$1') .replace(/(\/\/ NOTE:.*$)/gm, '$1') // Paragraphs .replace(/\n\n/g, '

    ') .replace(/^(.+)$/gm, '

    $1

    ') // Clean up .replace(/

    <\/p>/g, '') .replace(/

    ()/g, '$1') .replace(/(<\/h[1-3]>)<\/p>/g, '$1'); } export function DocumentPane({ content, onChange, phase, disabled }: DocumentPaneProps) { const [isEditing, setIsEditing] = React.useState(false); const renderedHtml = useMemo(() => renderMarkdown(content), [content]); if (phase === "implement") { // In implement phase, show read-only rendered view return (

    plan.md Implementing...
    ); } const filename = phase === "research" ? "research.md" : "plan.md"; return (
    {filename}
    {isEditing ? (