From 34ba41851752da108fbede66997cda0f814eb714 Mon Sep 17 00:00:00 2001 From: Clawd Date: Fri, 27 Feb 2026 21:44:59 -0800 Subject: Add detailed implementation plan with code snippets and TODO list --- plan.md | 1487 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1487 insertions(+) create mode 100644 plan.md diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..ff71487 --- /dev/null +++ b/plan.md @@ -0,0 +1,1487 @@ +# 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...
} +
+
+ +
+