From 9a636af9090b122db2e55737fca3e78550aab9df Mon Sep 17 00:00:00 2001 From: bndw Date: Sat, 28 Feb 2026 19:14:01 -0800 Subject: fix: scope artifacts to sessions --- plan.md | 1684 --------------------------------------------------------------- 1 file changed, 1684 deletions(-) delete mode 100644 plan.md (limited to 'plan.md') diff --git a/plan.md b/plan.md deleted file mode 100644 index 9f74c1c..0000000 --- a/plan.md +++ /dev/null @@ -1,1684 +0,0 @@ -# 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 ? ( -